Contact salesFree trial
Blog

Einführung von Vinego bei Upsun

VinegoGo
Teilen Sie

Vinego ist ein neuer Satz von sicherheitsrelevanten Go-Analysatoren, die wir intern verwenden. Falls Sie mit der Go-Linting-Szene nicht vertraut sind: Go bietet offizielle Schnittstellen für die Entwicklung von Code-Analysatoren, die die Grundlage für Community-Linter-Frameworks und -Bundles wie golangci-lint bilden.

Das Besondere an den neuen Vinego-Lintern ist, dass sie sich darauf konzentrieren, bestimmte Idiome zugunsten von nicht-idiomatischen, aber expliziteren und hoffentlich sichereren alternativen Mustern abzulehnen. Lassen Sie uns in diese eintauchen.

Nullwerte und der varinit-Linter

Nullwerte bezieht sich darauf, dass in Go alle Variablen gültige Werte haben, auch wenn sie nicht explizit initialisiert sind -einschließlich Strings (der leere String) und Structs (eine Struktur mit allen Feldern als Nullwerte). Im Grunde sind die beiden folgenden gleichwertig:

var a int
var a int = 0

Was ist, wenn eine Variable explizit initialisiert werden muss? Nach der allgemeinen Philosophie von Go (zitieren Sie mich nicht) würden Sie entweder:

  1. Code schreiben, der entweder den Nullwert als Sentinel behandelt (unter der Annahme, dass 0 für nicht initialisiert steht).
  2. So arbeiten, dass alle Werte ein OK-Verhalten erzeugen (Nullwerte werden natürlich durch die gewählte Implementierung behandelt).

Aber manchmal ist das nicht möglich. Betrachten Sie zum Beispiel diese Berechnung:

var desiredInstances uint32 if using_process1 { desiredInstances = process1(currentInstances, 45) } else { process2() } adjustInstances(desiredInstances)

Stellen Sie sich vor, dies sei echter Code mit einer komplexen Reihe von Verzweigungen, die bestimmen, welcher Algorithmus zur Berechnung des desiredInstances-Wertes verwendet wird, und nicht ein allzu abstraktes Spielzeugbeispiel.

Der else-Zweig im obigen Code vergisst, desiredInstances zu setzen, so dass er, wenn er ausgeführt wird, mit desiredInstances == 0 endet und adjustInstances ein unerwartetes Herunterfahren verursacht.

Der Go-Compiler wird bei diesem Code keinen Fehler machen, weil var desiredInstances int aus seiner Sicht vollständig initialisiert ist, und wenn der Nullwert ein Problem verursacht, liegt das daran, dass der Code nicht idiomatisch für Go ist. Aber in diesem Beispiel ist die Domäne von desiredInstances der gesamte Bereich von uint32, so dass es keinen Wert gibt, den wir als Sentinel für eine Nichtinitialisierung verwenden können, und jeder Standardwert könnte eine unerwartete Anpassung verursachen, so dass es keinen sicheren Standard gibt.

Eine Möglichkeit, dies zu umgehen, besteht darin, die Deklaration in var desiredInstances *int zu ändern, wodurch die Anzahl der verfügbaren Werte um einen erhöht wird (indem nil hinzugefügt wird), den wir als Sentinel verwenden können. Alle Verwendungen müssen zuerst dereferenziert werden(*desiredInstances), aber dies führt zu einer Panik, anstatt versehentlich alle unsere Instanzen zu zerstören, wenn wir eine Verzweigung verpassen, was eine Verbesserung darstellt.

In Panik zu geraten ist allerdings immer noch nicht toll. Es endet damit, dass es eine Laufzeitsicherung gegen etwas ist, das statisch falsch ist. (Beachten Sie, dass selbst wenn wir 0 als Sentinel für die Uninitialisierung verwenden könnten, es immer noch ein Laufzeitschutz wäre).

Die Lösung von Vinego

Vinego bietet für dieses Szenario den varinit-Linter an. Der varinit-Linter lehnt das Null-Wert-Idiom ab und betrachtet implizite Null-Initialisierungen wie var x int als tatsächlich uninitialisiert, und der Bericht liest solche Werte als falsch.

var x int if something() { x = 4 } else { somethingElse() } consume(x)

Die obige Meldung weist darauf hin, dass die Initialisierung von x in consume(x) im obigen else-Zweig fehlt. Dies geschieht durch die Verfolgung von Variablen durch den Kontrollflussgraphen des Codes und die Aufzeichnung von Pfadaufspaltungen mit gemischtem Initialisierungsstatus.

Auf diese Weise können Sie furchtlos verzweigen und komplexe Initialisierungen, Berechnungen usw. durchführen, ohne sich Sorgen machen zu müssen, dass sich unerwartete Nullwerte einschleichen, und Ihren Code so gestalten zu müssen, dass er den Schaden in solchen Szenarien erkennt und abmildert.

Strukturelle literale Nullwerte und Allfields

Aber es gibt noch eine weitere Quelle für Nullwerte: Strukturliterale.

Wann immer Sie eine struct mit einem struct-Literal teilweise spezifizieren, wird Go alle nicht spezifizierten Felder null-initialisieren. Zum Beispiel, während varinit überprüft, ob x initialisiert ist:

type MyStruct struct { Important int } var x MyStruct x = MyStruct{}

x.Important wird hier kritisch übersehen und erhält einen Nullwert.

Vinego's Lösung

allfields führt eine neue Annotation ein, mit der Sie dem Linter mitteilen können, dass alle Felder im Literal explizit initialisiert werden müssen:

// check:allfields type MyStruct struct { Important int } x := MyStruct{}

Das obige Beispiel führt zu einem Linterfehler, der mitteilt, dass Important nicht explizit initialisiert wurde. Sie können Felder als optional markieren (und damit eine implizite Initialisierung mit Nullwerten ermöglichen), etwa so:

// check:allfields type MyStruct struct { Unimportant int `optional: "1"` }

Mit varinit und allfields sollten Sie sich keine Sorgen mehr über Nullwert-Fehler machen müssen!

Erinnern Sie sich daran, Fehler mit dem capturederr lint zu behandeln

Es gibt eigentlich schon einen Linter(staticcheck) in golangci-lint, der prüft, ob alle errsverbraucht werden, aber der Linter gibt die Analyse an Funktionsgrenzen auf.

Wenn Sie also versehentlich einen Err aus einem äußeren Bereich einfangen, hört die Analyse auf, Sie auf den schlechten Code hinzuweisen:

x, err := operation1() if err != nil { ... } go func() { err = operation2() // kein Fehler! }

Im obigen Beispiel wird err nie innerhalb von func() verwendet, aber staticcheck erkennt dies nicht, da err außerhalb deklariert wurde.

Vinego's Lösung

Capturederr markiert in solchen Fällen einfach alle Verwendungen von Fehlervariablen in lokalen Funktionsdefinitionen, bei denen die Fehlervariable nicht lokal deklariert, sondern von der äußeren Funktion übernommen wurde. In den allermeisten Fällen ist dies eindeutig ein falsches Verhalten -Fehler müssen behandelt werden, um die Ausführung lokal abzubrechen, bevor abhängiger Code ausgeführt wird, und die meisten Rückrufe haben eine Fehlerrückgabe.

In diesem Beispiel wird eine Fehlermeldung ausgegeben, die besagt, dass err in go func() .... gefangen ist . Wenn Sie das beheben, indem Sie es in err := operation2() ändern, wird staticcheck wieder funktionieren und sicherstellen, dass Sie den Fehler richtig behandeln.

Versehentliche Typverwechslungen und der explicitcast lint

Es gibt ein "newtype"-Paradigma der Verwendung von dünnen Typumhüllungen um bestehende Typen, wie

Typ Mebibyte uint64 Typ Gibibyte uint64

Diese bieten eine bessere Dokumentation als die direkte Verwendung von uint64 in Variablendeklarationen, Funktionssignaturen und Strukturdefinitionen, da die Einheit klar kommuniziert wird. Idealerweise werden diese auch streng geprüft, so dass Sie nicht versehentlich Mebibyte übergeben können, wo Gibibyte erwartet wird und umgekehrt.

Unter Umständen gibt go einen Fehler aus, wenn man z.B. den falschen Typ verwendet:

func Resize(newSize Mebibyte) { ... } newSize := 4 Resize(newSize) // Fehler!

In anderen Situationen konvertiert Go jedoch implizit in den Typ, wie im folgenden Beispiel zu sehen ist:

func ResizeAfterDelay(newSize Mebibyte, delay time.Duration) { ... } ResizeAfterDelay(3600, 256) // Hoppla! Verzögerung und Größe verwechselt, Verzögerung hat falsche Einheiten, kein Fehler

Dadurch wird die zusätzliche Sicherheit der neuen eindeutigen Typen verworfen.

Die Art und Weise, wie Go diese Szenarien unterscheidet, hat mit literalen Werten zu tun. Falls Sie mit Literalen nicht vertraut sind: Literale sind der Teil der Syntax, der nicht berechnete Werte einführt: 43, true, "alles in diesem Artikel ist eine Lüge!", MyStruct{}.

Diese Werte haben noch keine konkreten Typen. 43 ist zwar eine ganze Zahl, könnte aber auch ein int32 oder ein uint64 oder ein anderer int-Typ sein.

Wenn Sie ein Literal in einem Ausdruck verwenden, es einer Variablen zuweisen, es zurückgeben oder es als Funktionsargument übergeben, weist Go ihm einen Typ aus dem Kontext zu.

Im obigen Beispiel sind sowohl Mebibyte als auch time.Duration newtypes über primitive Ganzzahlen, so dass 3600 den Typ Mebibyte und 256 time.Duration annimmt. Bei newSize := 4 hat Go mangels expliziter kontextbezogener Typinformationen den Standard-Ganzzahltyp int verwendet (Variablen haben immer konkrete Typen).

Vinego's Lösung

Der explizite Cast-Lint erkennt Literale, die kontextuell typisiert sind, und markiert sie, so dass ein expliziter Cast erforderlich ist, um ihn zu beruhigen:

ResizeAt(time.Seconds * 3600, Mebibyte(256))

Dadurch werden die Typsicherheitsprobleme (falsche Zeiteinheit, Argumenttransport) im obigen Beispiel aufgedeckt - Sie erhalten nun einen Fehler, der besagt, dass das erste Argument Mebibyte und das zweite Duration sein sollte.

Der loopvariableref lint

Mit schlechtem Timing bringe ich Ihnen den letzten Linter: loopvariableref.

Wenn Sie die Go-Versionsankündigungen verfolgt haben, werden Sie wissen, dass Go 1.22 das Root-Problem behebt und der Linter daher nicht mehr benötigt wird, aber er könnte immer noch nützlich sein für Codebasen, die aus irgendeinem Grund nicht upgraden können (vielleicht?).

Dieser Linter prüft im Wesentlichen, ob Referenzen oder Captures von Schleifenvariablen vorhanden sind, die es erlauben, dass die Lebensdauer der Schleifenvariablen die Schleife überschreitet und ein unerwartetes Verhalten zeigt, und markiert diese.

Warnungen

Keiner dieser Liner hat ein perfektes Signal. Zum Beispiel geht varinit vom schlimmsten Fall aus, wenn Captures und Goroutines vorhanden sind, da sich in diesen Fällen die Reihenfolge der Initialisierung zur Laufzeit ändern kann, so dass Sie vielleicht einen vollkommen gültigen Code haben (z.B. die Verwendung einer WaitGroup, um sicherzustellen, dass eine Goroutine beendet wird, bevor eine Variable verwendet wird), aber der Linter würde ihn trotzdem kennzeichnen.

Außerdem können einige, wie z.B. capturederr, legitimen Code markieren und erfordern zusätzlichen Aufwand, um ihn zu umgehen: z.B. wenn man den wortwörtlichen Fehler einer Operation in einem Callback-Out weitergeben muss und keine anderen Mechanismen zur Verfügung stehen.

Davon abgesehen, sollten diese helfen, einige ziemlich große Sicherheitslücken zu schließen und Ihnen bessere Basisgarantien zu geben. Und die erforderlichen Workarounds sind hoffentlich nicht zu lästig.

Go Git it

Sie können es unter https://github.com/upsun/vinego erhalten - dieREADME enthält Installations- und Nutzungsanweisungen sowie weitere Details zu jedem Linter.

Es wird als Docker-Image verteilt, das das komplette golangci-lint als Go-Komplettlösung enthält, oder Sie können es selbst bauen.

Halten Sie Ausschau nach weiteren Blog-Beiträgen über die Architektur des Go-Analysators, Tipps und Tricks für die Arbeit mit den Schnittstellen und golangci-lint und wie die Vinego-Linters ihre Analysen durchführen.

Ihr größtes Werk
steht vor der Tür

Kostenloser Test
Discord
© 2025 Platform.sh. All rights reserved.