
Hallo Habr. Mein Name ist Sergey Rudachenko, ich bin technischer Experte bei Roistat. In den letzten zwei Jahren hat unser Team verschiedene Teile des Projekts in Microservices on Go übersetzt. Sie werden von mehreren Teams entwickelt, daher mussten wir einen Qualitätsbalken für harten Code festlegen. Zu diesem Zweck verwenden wir mehrere Tools. In diesem Artikel konzentrieren wir uns auf eines davon - die statische Analyse.
Bei der statischen Analyse wird der Quellcode mithilfe spezieller Dienstprogramme automatisch überprüft. In diesem Artikel werden die Vorteile erläutert, beliebte Tools kurz beschrieben und Anweisungen zur Implementierung gegeben. Es lohnt sich zu lesen, wenn Sie auf ähnliche Tools überhaupt nicht gestoßen sind oder sie unsystematisch verwenden.
In Artikeln zu diesem Thema wird häufig der Begriff "Linter" verwendet. Für uns ist dies ein praktischer Name für einfache Werkzeuge zur statischen Analyse. Die Aufgabe des Linters besteht darin, nach einfachen Fehlern und falschem Design zu suchen.
Warum werden Linters benötigt?
Wenn Sie in einem Team arbeiten, führen Sie höchstwahrscheinlich Codeüberprüfungen durch. In der Überprüfung übersprungene Fehler sind potenzielle Fehler. Verpasste einen nicht behandelten error
- erhalten Sie keine informative Nachricht und Sie werden blind nach dem Problem suchen. Beim Typ-Casting falsch oder auf Null-Karte gedreht - noch schlimmer, die Binärdatei gerät in Panik.
Die oben beschriebenen Fehler können zu Code-Konventionen hinzugefügt werden , aber es ist nicht so einfach, sie beim Lesen der Pull-Anforderung zu finden, da der Prüfer den Code lesen muss. Wenn Sie keinen Compiler im Kopf haben, werden einige der Probleme trotzdem in den Kampf ziehen. Darüber hinaus lenkt die Suche nach geringfügigen Fehlern von der Überprüfung der Logik und Architektur ab. In einiger Entfernung wird die Unterstützung eines solchen Codes teurer. Wir schreiben in einer statisch typisierten Sprache, es ist seltsam, sie nicht zu verwenden.
Beliebte Tools
Die meisten Tools für die statische Analyse verwenden die Pakete go/ast
und go/parser
. Sie bieten Funktionen zum Parsen der Syntax von .go-Dateien. Der Standardausführungsthread (z. B. für das Dienstprogramm golint) lautet wie folgt:
- Liste der Dateien aus erforderlichen Paketen wird geladen
parser.ParseFile(...) (*ast.File, error)
wird für jede Datei ausgeführt- sucht für jede Datei oder jedes Paket nach unterstützten Regeln
- Die Überprüfung durchläuft beispielsweise jede Anweisung wie folgt:
f, err := parser.ParseFile() ast.Walk(func (n *ast.Node) { switch v := node.(type) { case *ast.FuncDecl: if strings.Contains(v.Name, "_") { panic("wrong function naming") } } }, f)
Zusätzlich zu AST gibt es Single Static Assignment (SSA). Dies ist eine komplexere Methode zum Parsen von Code, die eher mit einem Ausführungsthread als mit Syntaxkonstrukten arbeitet. In diesem Artikel werden wir nicht im Detail darauf eingehen. Sie können die Dokumentation lesen und sich das Beispiel für das Dienstprogramm stackcheck ansehen .
Als nächstes werden nur beliebte Dienstprogramme berücksichtigt, die nützliche Überprüfungen für uns durchführen.
gofmt
Dies ist das Standarddienstprogramm aus dem go-Paket, das nach Stilübereinstimmungen sucht und diese automatisch beheben kann. Die Einhaltung des Stils ist für uns eine zwingende Voraussetzung, daher ist die Überprüfung der Gofmt in allen unseren Projekten enthalten.
typecheck
Typecheck prüft, ob der Code mit dem Typ übereinstimmt, und unterstützt den Hersteller (im Gegensatz zu gotype). Der Start ist erforderlich, um die Kompilierung zu überprüfen, bietet jedoch keine vollständigen Garantien.
Geh zum Tierarzt
Das Dienstprogramm go vet ist Teil des Standardpakets und wird vom Go-Team empfohlen. Überprüft eine Reihe häufiger Fehler, zum Beispiel:
- Missbrauch von printf und ähnlichen Funktionen
- falsche Build-Tags
- Vergleich von Funktion und Null
Golint
Golint wurde vom Go-Team entwickelt und validiert Code basierend auf den Dokumenten Effective Go und CodeReviewComments . Leider gibt es keine detaillierte Dokumentation, aber der Code zeigt, dass Folgendes überprüft wird:
f.lintPackageComment() f.lintImports() f.lintBlankImports() f.lintExported() f.lintNames() f.lintVarDecls() f.lintElses() f.lintRanges() f.lintErrorf() f.lintErrors() f.lintErrorStrings() f.lintReceiverNames() f.lintIncDec() f.lintErrorReturn() f.lintUnexportedReturn() f.lintTimeNames() f.lintContextKeyTypes() f.lintContextArgs()
statische Prüfung
Die Entwickler selbst präsentieren staticcheck als verbesserten Tierarzt. Es gibt viele Schecks, sie sind in Gruppen unterteilt:
- Missbrauch von Standardbibliotheken
- Multithreading-Probleme
- Probleme mit Tests
- nutzloser Code
- Leistungsprobleme
- zweifelhafte Designs
gosimple
Es ist spezialisiert auf die Suche nach vereinfachbaren Strukturen, zum Beispiel:
Vorher ( Golint-Quellcode )
func (f *file) isMain() bool { if ffName.Name == "main" { return true } return false }
Nachher
func (f *file) isMain() bool { return ffName.Name == "main" }
Die Dokumentation ähnelt der statischen Prüfung und enthält detaillierte Beispiele.
Fehler überprüfen
Von Funktionen zurückgegebene Fehler können nicht ignoriert werden. Die Gründe sind im verbindlichen Dokument Effective Go ausführlich beschrieben. Errcheck überspringt den folgenden Code nicht:
json.Unmarshal(text, &val) f, _ := os.OpenFile()
Gas
Findet Schwachstellen im Code: fest codierte Zugriffe, SQL-Injektionen und Verwendung unsicherer Hash-Funktionen.
Beispiele für Fehler:
bösartig
In Go wirkt sich die Reihenfolge der Felder in Strukturen auf den Speicherverbrauch aus. Maligned findet nicht optimale Sortierung. Mit dieser Reihenfolge der Felder:
struct { a bool b string c bool }
Die Struktur belegt 32 Bit im Speicher, da nach den Feldern a und c leere Bits hinzugefügt werden.

Wenn wir die Sortierung ändern und zwei Bool-Felder zusammenfügen, benötigt die Struktur nur 24 Bit:

Originalbild auf Stapelüberlauf
goconst
Magische Variablen im Code spiegeln nicht die Bedeutung wider und erschweren das Lesen. Goconst findet Literale und Zahlen, die mindestens zweimal im Code vorkommen. Bitte beachten Sie, dass oft schon eine einmalige Verwendung ein Fehler sein kann.
Gocyclo
Wir betrachten die zyklomatische Komplexität von Code als eine wichtige Metrik. Gocycle zeigt Komplexität für jede Funktion. Es können nur Funktionen angezeigt werden, die den angegebenen Wert überschreiten.
gocyclo -over 7 package/name
Wir haben für uns selbst einen Schwellenwert von 7 gewählt, weil wir keinen Code mit einer höheren Komplexität gefunden haben, für den kein Refactoring erforderlich war.
Toter Code
Es gibt verschiedene Dienstprogramme zum Auffinden nicht verwendeten Codes, deren Funktionalität sich teilweise überschneiden kann.
- Ineffassign: Überprüft nutzlose Zuordnungen
func foo() error { var res interface{} log.Println(res) res, err := loadData()
- Deadcode: Findet nicht verwendete Funktionen
- unbenutzt: Findet unbenutzte Funktionen, ist aber besser als Deadcode
func unusedFunc() { formallyUsedFunc() } func formallyUsedFunc() { }
Infolgedessen zeigt unbenutzt auf beide Funktionen gleichzeitig und Deadcode nur auf unusedFunc. Dadurch wird der zusätzliche Code in einem Durchgang gelöscht. Auch unbenutzt findet nicht verwendete Variablen und Strukturfelder.
- varcheck: findet nicht verwendete Variablen
- nicht konvertieren: findet nutzlose Typkonvertierungen
var res int return int(res)
Wenn Sie keine Zeit sparen müssen, um Überprüfungen zu starten, ist es besser, alle zusammen auszuführen. Wenn eine Optimierung erforderlich ist, empfehle ich die Verwendung von nicht verwendet und nicht konvertiert.
Wie bequem es zu konfigurieren ist
Das Ausführen der oben genannten Tools nacheinander ist unpraktisch: Fehler werden in einem anderen Format ausgegeben, die Ausführung nimmt viel Zeit in Anspruch. Das Überprüfen eines unserer Dienste mit einer Größe von ~ 8000 Codezeilen dauerte mehr als zwei Minuten. Sie müssen die Dienstprogramme auch separat installieren.
Es gibt Aggregationsdienstprogramme, um dieses Problem zu lösen, z. B. Goreporter und Gometalinter . Goreporter rendert den Bericht in HTML und Gometalinter schreibt in die Konsole.
Gometalinter wird immer noch in einigen großen Projekten verwendet (z. B. Docker ). Er weiß, wie man alle Dienstprogramme mit einem einzigen Befehl installiert, parallel ausführt und Fehler gemäß der Vorlage formatiert. Die Ausführungszeit im oben genannten Dienst wurde auf eineinhalb Minuten reduziert.
Die Aggregation funktioniert nur durch das genaue Zusammentreffen des Fehlertextes, daher sind wiederholte Fehler am Ausgang unvermeidlich.
Im Mai 2018 erschien das Golangci-Lint- Projekt auf dem Github, das Gometalinter in seiner Bequemlichkeit weit übertrifft:
- Die Ausführungszeit für dasselbe Projekt wurde auf 16 Sekunden (8 Mal) reduziert.
- fast keine doppelten Fehler
- yaml config löschen
- schöne Fehlerausgabe mit einer Codezeile und einem Zeiger auf ein Problem

- Es müssen keine zusätzlichen Dienstprogramme installiert werden
Jetzt wird die Geschwindigkeitssteigerung durch die Wiederverwendung von SSA und Loader erreicht. Programm : In Zukunft ist auch geplant, den AST-Baum wiederzuverwenden, über den ich am Anfang des Abschnitts Tools geschrieben habe.
Zum Zeitpunkt des Schreibens dieses Artikels auf hub.docker.com gab es kein Bild mit Dokumentation, daher haben wir unser eigenes Bild erstellt, das gemäß unseren Vorstellungen von Bequemlichkeit angepasst wurde. In Zukunft wird sich die Konfiguration ändern. Für die Produktion empfehlen wir daher, sie durch Ihre eigene zu ersetzen. Fügen Sie dazu einfach die Datei .golangci.yaml zum Stammverzeichnis des Projekts hinzu ( ein Beispiel befindet sich im golangci-lint-Repository).
PACKAGE=package/name docker run --rm -t \ -v $(GOPATH)/src/$(PACKAGE):/go/src/$(PACKAGE) \ -w /go/src/$(PACKAGE) \ roistat/golangci-lint
Dieser Befehl kann das gesamte Projekt testen. Wenn es sich beispielsweise in ~/go/src/project
, ändern Sie den Wert der Variablen in PACKAGE=project
. Die Validierung funktioniert rekursiv für alle internen Pakete.
Bitte beachten Sie, dass dieser Befehl nur bei Verwendung des Herstellers ordnungsgemäß funktioniert.
Implementierung
Alle unsere Entwicklungsdienste verwenden Docker. Jedes Projekt wird ohne installierte go-Umgebung ausgeführt. Verwenden Sie zum Ausführen der Befehle das Makefile und fügen Sie den Befehl lint hinzu:
lint: @docker run --rm -t -v $(GOPATH)/src/$(PACKAGE):/go/src/$(PACKAGE) -w /go/src/$(PACKAGE) roistat/golangci-lint
Jetzt wird die Prüfung mit folgendem Befehl gestartet:
make lint
Es gibt eine einfache Möglichkeit, fehlerhafte Codeeingabe für den Master zu blockieren - erstellen Sie einen Pre-Receive-Hook. Es ist geeignet, wenn:
- Sie haben ein kleines Projekt und wenige Abhängigkeiten (oder sie befinden sich im Repository).
- Es ist kein Problem für Sie, einige Minuten zu warten, bis der Befehl
git push
abgeschlossen ist
Anweisungen zur Hook- Konfiguration: Gitlab , Bitbucket Server , Github Enterprise .
In anderen Fällen ist es besser, CI zu verwenden und Zusammenführungscode zu verbieten, bei dem mindestens ein Fehler vorliegt. Wir tun genau das und fügen den Linter vor den Tests hinzu.
Fazit
Die Einführung systematischer Überprüfungen hat den Überprüfungszeitraum erheblich verkürzt. Eine andere Sache ist jedoch wichtiger: Jetzt können wir die meiste Zeit über das Gesamtbild und die Architektur diskutieren. Auf diese Weise können Sie über die Entwicklung des Projekts nachdenken, anstatt Löcher zu stopfen.