Statische Analyse in Go: Wie wir Zeit sparen, wenn wir Code überprüfen


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:


 //    IP  l, err := net.Listen("tcp", ":2000") //  sql  q := fmt.Sprintf("SELECT * FROM foo where name = '%s'", name) q := "SELECT * FROM foo where name = " + name //     import "crypto/md5" 

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.


Bild


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


Bild


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() //  res    return err } 

  • 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) // unconvert error 

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:


  1. Sie haben ein kleines Projekt und wenige Abhängigkeiten (oder sie befinden sich im Repository).
  2. 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.

Source: https://habr.com/ru/post/de413175/


All Articles