Lucas F. Costa Menschliche Gedanken über exakte Wissenschaften
Hallo, alle zusammen! Wie ihr vielleicht schon bemerkt habt, interessiere ich mich sehr für Go und da ich mich in diese Sprache verliebt habe, möchte ich gerne öfter darüber schreiben.
Wenn ihr Go noch nicht kennt, solltet ihr es unbedingt lernen. Go ist großartig!
Go ist eine relativ junge Sprache, und wenn es um Vendoring und Abhängigkeitsmanagement geht, ist sie noch nicht wirklich ausgereift. Die Go-Gemeinschaft hat jedoch einige Praktiken übernommen und Tools entwickelt, um diesen Bedarf zu decken. Darüber werden wir heute sprechen.
Das Go-Ökosystem verstehen
Go zielt darauf ab, komplexe Aufgaben mit einfachen Mitteln zu bewältigen und so die Sprache unterhaltsamer und produktiver zu machen. Seit den Anfängen der Sprache entschied sich das Go-Team für die Verwendung von String-Literalen, um die Import-Syntax beliebig zu machen und zu beschreiben, was importiert wird.
Das steht in den eigenen Unterlagen:
Ein explizites Ziel für Go war es von Anfang an, Go-Code nur mit den Informationen zu bauen, die im Quelltext selbst zu finden sind, ohne ein Makefile oder einen der vielen modernen Ersetzungen für Makefiles schreiben zu müssen. Wenn Go eine Konfigurationsdatei bräuchte, um zu erklären, wie man sein Programm baut, dann wäre Go gescheitert.
Um dieses Ziel zu erreichen, bevorzugt das Go-Team Konventionen gegenüber Konfigurationen. Dies sind die Konventionen, die bei der Paketverwaltung angewendet werden:
- Der Importpfad wird immer auf bekannte Weise von der URL des Quellcodes abgeleitet. Deshalb sehen Ihre Importe so aus:
github.com/author/pkgname
. Auf diese Weise erhalten wir auch einen automatisch verwalteten Namensraum, da diese Online-Dienste bereits eindeutige Pfade für jedes Paket verwalten. - Der Ort, an dem wir Pakete in unserem Dateisystem speichern, wird auf bekannte Weise vom Importpfad abgeleitet. Wenn Sie danach suchen, wo
go
die von Ihnen heruntergeladenen Pakete gespeichert werden, können Sie sie der URL zuordnen, von der sie heruntergeladen wurden. - Jedes Verzeichnis in einem Quellbaum entspricht einem einzelnen Paket. Das macht es einfacher, den Quellcode für bestimmte Pakete zu finden und hilft Ihnen, Ihren Code auf eine standardisierte Weise zu organisieren. Indem wir die Ordnerstruktur an die Paketstruktur binden, brauchen wir uns nicht um beides gleichzeitig zu kümmern, weil Dateisystemwerkzeuge zu Paketverwaltungswerkzeugen werden.
- Das Paket wird nur mit Informationen aus dem Quellcode gebaut. Das bedeutet kein
makefiles
und keinconfiguration
und befreit Sie davon, spezielle (und wahrscheinlich komplizierte) Toolchains einsetzen zu müssen.
Nachdem wir das gesagt haben, ist es leicht zu verstehen, warum der go get
-Befehl so funktioniert, wie er funktioniert.
Die Befehle der Go-Tools verstehen
Bevor wir uns mit diesen Befehlen beschäftigen, sollten wir verstehen, was „$GOPATH
“ ist.
Das $GOPATH
ist eine Umgebungsvariable, die auf ein Verzeichnis verweist, das als Arbeitsverzeichnis betrachtet werden kann. Es wird deine Quellcodes, kompilierte Pakete und lauffähige Binärdateien enthalten.
go get
Der go get
Befehl ist einfach und funktioniert fast wie ein git clone
. Das Argument, das Sie an go get
übergeben müssen, ist eine einfache Repository-URL. In diesem Beispiel werden wir den Befehl verwenden: go get https://github.com/golang/oauth2
.
Wenn Sie diesen Befehl ausführen, holt go
einfach das Paket von der angegebenen URL und legt es in Ihrem $GOPATH
-Verzeichnis ab. Wenn Sie zu Ihrem Ordner $GOPATH
navigieren, sehen Sie nun, dass Sie einen Ordner in src/github.com/golang/oauth2
haben, der die Quelldateien des Pakets enthält, und ein kompiliertes Paket im Verzeichnis pkg
(zusammen mit seinen Abhängigkeiten).
Wenn Sie go get
ausführen, sollten Sie daran denken, dass alle heruntergeladenen Pakete in einem Verzeichnis abgelegt werden, das der URL entspricht, die Sie zum Herunterladen verwendet haben.
Es stehen auch eine Reihe anderer Flags zur Verfügung, wie -u
, das ein Paket aktualisiert, oder -insecure
, das Ihnen erlaubt, Pakete über unsichere Verfahren wie HTTP herunterzuladen. Sie können mehr über die „fortgeschrittene“ Verwendung des go get
-Befehls unter diesem Link lesen.
Außerdem aktualisiert der go get
-Befehl laut go help gopath
auch die Untermodule der Pakete, die Sie erhalten.
go install
Wenn go install
im Quellverzeichnis eines Pakets ausgeführt wird, kompiliert es die neueste Version dieses Pakets und alle seine Abhängigkeiten im Verzeichnis pkg
.
go build
Go build ist für das Kompilieren der Pakete und ihrer Abhängigkeiten verantwortlich, aber es installiert nicht die Ergebnisse!
Understanding Vendoring
Wie Sie vielleicht anhand der Art und Weise, wie Go seine Abhängigkeiten speichert, bemerkt haben, hat dieser Ansatz zur Verwaltung von Abhängigkeiten einige Probleme.
Erstens sind wir nicht in der Lage festzustellen, welche Version eines Pakets wir benötigen, es sei denn, es befindet sich in einem völlig anderen Repository, andernfalls holt go get
immer die neueste Version eines Pakets. Das bedeutet, wenn jemand eine bahnbrechende Änderung an seinem Paket vornimmt und es nicht in ein anderes Repository stellt, werden Sie und Ihr Team in Schwierigkeiten geraten, weil Sie am Ende verschiedene Versionen desselben Pakets holen könnten, was dann zu „funktioniert auf meinem Rechner“-Problemen führt.
Ein weiteres großes Problem ist, dass Sie aufgrund der Tatsache, dass go get
Pakete in die Wurzel Ihres src
-Verzeichnisses installiert, nicht in der Lage sein werden, verschiedene Versionen Ihrer Abhängigkeiten für jedes Ihrer Projekte zu haben. Das bedeutet, dass man keine Projekte haben kann, die von verschiedenen Versionen desselben Pakets abhängen, man muss entweder die eine oder die andere Version haben.
Um diese Probleme zu entschärfen, hat das Go-Team seit Go 1.5 eine vendoring
-Funktion eingeführt. Diese Funktion ermöglicht es Ihnen, den Code Ihrer Abhängigkeit für ein Paket in sein eigenes Verzeichnis zu legen, so dass es immer die gleichen Versionen für alle Builds erhalten kann.
Angenommen, Sie haben ein Projekt namens awesome-project
, das von popular-package
abhängt. Um zu garantieren, dass jeder in deinem Team die gleiche Version von popular-package
verwendet, kannst du seinen Quellcode in $GOPATH/src/awesome-project/vendor/popular-package
einfügen. Das wird funktionieren, weil Go
dann versuchen wird, den Pfad deiner Abhängigkeiten aufzulösen, indem es mit dem Verzeichnis vendor
seines eigenen Ordners beginnt (wenn es mindestens eine .go
-Datei hat), anstatt mit $GOPATH/src
. Dies wird auch Ihre Builds deterministisch (reproduzierbar) machen, da sie immer die gleiche Version von popular-package
.
Es ist auch wichtig zu beachten, dass der go get
Befehl die heruntergeladenen Pakete nicht automatisch in den vendor
Ordner legt. Dies ist eine Aufgabe für Vendoring-Werkzeuge.
Wenn Sie Vendoring verwenden, können Sie die gleichen Importpfade verwenden, wie wenn Sie es nicht tun würden, da Go
immer versuchen wird, Abhängigkeiten im nächstgelegenen vendor
Verzeichnis zu finden. Es besteht keine Notwendigkeit, vendor/
einem der Importpfade voranzustellen.
Um zu verstehen, wie Vendoring funktioniert, müssen wir den Algorithmus verstehen, der von Go verwendet wird, um Importpfade aufzulösen, und das ist der folgende:
- Suche nach dem Import im lokalen
vendor
-Verzeichnis (falls vorhanden) - Wenn wir das Paket nicht im lokalen
vendor
-Verzeichnis finden können, gehen wir in den übergeordneten Ordner und versuchen, es dort imvendor
-Verzeichnis zu finden (falls vorhanden - Wir wiederholen Schritt 2 bis wir $GOPATH/src erreichen
- Wir suchen das importierte Paket unter $GOROOT
- Wenn wir dieses Paket nicht unter $GOROOT finden können, suchen wir es in unserem $GOPATH/src Ordner
Grundsätzlich, bedeutet dies, dass jedes Paket seine eigenen Abhängigkeiten haben kann, die in seinem eigenen Herstellerverzeichnis aufgelöst werden. Wenn Sie z.B. von Paket x
und Paket y
abhängig sind und Paket x
auch von Paket y
abhängt, aber eine andere Version davon benötigt, können Sie Ihren Code trotzdem ohne Probleme ausführen, weil x
in seinem eigenen Ordner vendor
nach y
sucht, während Ihr Paket im Vendor-Ordner Ihres Projekts nach y
sucht.
Nun ein praktisches Beispiel. Nehmen wir an, Sie haben diese Ordnerstruktur:
$GOPATH src/ github.com/user/package-one/ one.go myproject main.go vendor/ github.com/user/package-one/ one.go client/ client.go vendor/ github.com/user/package-one/ server/ server.go vendor/ github.com/user/package-one/ one.go
Wenn wir github.com/user/package-one
von innerhalb von main.go importieren, würde es die Version dieses Pakets im Verzeichnis vendor
im gleichen Ordner auflösen:
$GOPATH src/ github.com/user/package-one/ one.go myproject main.go <-- Importing package-one from here vendor/ github.com/user/package-one/ <-- resolves to here one.go client/ client.go vendor/ github.com/user/package-one/ server/ server.go vendor/ github.com/user/package-one/ one.go
Wenn wir nun das gleiche Paket in client.go
importieren, wird es auch dieses Paket im Ordner vendor
in seinem eigenen Verzeichnis auflösen:
$GOPATH src/ github.com/user/package-one/ one.go myproject main.go vendor/ github.com/user/package-one/ one.go client/ client.go <-- Importing package-one from here vendor/ github.com/user/package-one/ <-- resolves to here server/ server.go vendor/ github.com/user/package-one/ one.go
Das Gleiche passiert, wenn wir dieses Paket in der Datei server.go
importieren:
$GOPATH src/ github.com/user/package-one/ one.go myproject main.go vendor/ github.com/user/package-one/ one.go client/ client.go vendor/ github.com/user/package-one/ server/ server.go <-- Importing package-one from here vendor/ github.com/user/package-one/ <-- resolves to here one.go
Understanding Dependency Management
Nachdem wir nun all diese Dinge darüber gelernt haben, wie Go
Importe und Vendoring handhabt, ist es an der Zeit, endlich über das Dependency Management zu sprechen.
Das Tool, das ich derzeit benutze, um Abhängigkeiten in meinen eigenen Projekten zu verwalten, heißt godep
. Es scheint sehr populär zu sein und funktioniert gut für mich, daher empfehle ich dir, es auch zu benutzen.
Es ist so aufgebaut, wie vendoring
funktioniert. Alles, was du tun musst, um es zu benutzen, ist den Befehl godep save
zu benutzen, wann immer du deine Abhängigkeiten in deinem vendor
-Ordner speichern willst.
Wenn du godep save
ausführst, wird godep
eine Liste deiner aktuellen Abhängigkeiten in Godeps/Godeps.json
speichern und dann ihren Quellcode in den vendor
-Ordner kopieren. Es ist auch wichtig zu wissen, dass diese Abhängigkeiten auf deinem Rechner installiert sein müssen, damit godep
sie kopieren kann.
Jetzt kannst du den Ordner vendor
und seinen Inhalt übertragen, um sicherzustellen, dass jeder die gleichen Versionen der gleichen Pakete hat, wenn du dein Paket ausführst.
Ein weiterer interessanter Befehl ist godep restore
, der die in der Godeps/Godeps.json
-Datei angegebenen Versionen in den $GOPATH
-Ordner installiert.
Um eine Abhängigkeit zu aktualisieren, musst du sie nur mit go get -u
aktualisieren (wie wir bereits besprochen haben) und dann godep save
ausführen, damit godep
die Godeps/Godeps.json
-Datei aktualisiert und die benötigten Dateien in das vendor
-Verzeichnis kopiert.
Ein paar Gedanken zur Art und Weise, wie Go mit Abhängigkeiten umgeht
Am Ende dieses Blog-Beitrags möchte ich noch meine eigene Meinung über die Art und Weise, wie Go mit Abhängigkeiten umgeht, hinzufügen.
Ich denke, dass die Entscheidung von Go, externe Repositories zu verwenden, um die Namespaces von Paketen zu handhaben, großartig war, weil es die gesamte Paketauflösung viel einfacher macht, indem es die Dateisystemkonzepte mit den Namespace-Konzepten verbindet. Das ganze Ökosystem funktioniert dadurch unabhängig, weil wir jetzt einen dezentralisierten Weg haben, um Pakete zu holen.
Die dezentralisierte Paketverwaltung hat jedoch einen Preis, der darin besteht, dass wir nicht in der Lage sind, alle Knoten zu kontrollieren, die Teil dieses „Netzwerks“ sind. Wenn jemand zum Beispiel beschließt, sein Paket aus github
herauszunehmen, können plötzlich Hunderte, Tausende oder sogar Millionen von Builds fehlschlagen. Namensänderungen könnten den gleichen Effekt haben.
In Anbetracht der Hauptziele von Go macht dies absolut Sinn und ist ein absolut fairer Kompromiss. Bei Go geht es um Konventionen statt um Konfiguration und es gibt keinen einfacheren Weg, Abhängigkeiten zu handhaben, als die Art und Weise, wie es derzeit funktioniert.
Natürlich könnten einige Verbesserungen vorgenommen werden, wie z.B. die Verwendung von Git-Tags, um bestimmte Versionen von Paketen abzurufen und den Benutzern zu erlauben, anzugeben, welche Versionen ihre Pakete verwenden sollen. Es wäre auch cool, wenn man diese Abhängigkeiten abrufen könnte, anstatt sie in der Versionsverwaltung auszuchecken. Das würde es uns ermöglichen, schmutzige Diffs zu vermeiden, die nur Änderungen am Code im vendor
-Verzeichnis enthalten, und das gesamte Repository sauberer und schlanker zu machen.
Kontaktieren Sie mich!
Wenn Sie irgendwelche Zweifel oder Gedanken haben oder mit irgendetwas, das ich geschrieben habe, nicht einverstanden sind, teilen Sie sie mir bitte in den Kommentaren unten mit oder erreichen Sie mich unter @thewizardlucas auf Twitter. Ich würde gerne hören, was ihr zu sagen habt, und mich korrigieren, wenn ich Fehler gemacht habe.
Zu guter Letzt solltet ihr euch diesen großartigen Vortrag von Wisdom Omuya auf der GopherCon 2016 ansehen, in dem er erklärt, wie Go Vendoring funktioniert, und auch einige Details über sein Innenleben aufzeigt.
Danke fürs Lesen!