Versionierte Packages mit Go 1.11 Modulen

Go Modules: Was es mit den neuen Modulen in Go auf sich hat

Jan Stamer

© Reneé French (CC BY 3.0 DE / modifiziert)

Go Packages kennen keine Versionen und keine Abhängigkeiten. Noch nicht. Mit Go 1.11 und dem experimentellen Support für versionierte Go-Module ändert sich das. Go-Module bringen native Unterstützung für Versionen und Module in Go als festen Bestandteil zur Go Toolchain. Go Modules haben den Anspruch, Community-Lösungen wie dep oder glide abzulösen und eine neue einheitliche Lösung zu schaffen. Aber gelingt das auch wirklich? Mal sehen…

Ein Go-Projekt startet mit einem Package. Noch ohne Version und Abhängigkeiten. Die Abhängigkeiten kommen schnell hinzu. Als Http Router nutzen wir das Package github.com/gorilla/mux, also: go get github.com/gorilla/mux. Weil wir XML verarbeiten noch etree, also go get github.com/beevik/etree. Das Projekt nimmt Fahrt auf, ein neuer Mitarbeiter kommt dazu. Nicht lange und wir haben Version 1.0.3 in Produktion, und ein ganzes Team entwickelt Version 1.1.0.

Aber was ist jetzt los? Auf einmal schlagen diverse Tests fehl – die XML-Verarbeitung geht nicht mehr! Da hat doch keiner was geändert, oder? Naja, stimmt nicht ganz. Keiner aus dem Team hat etwas geändert, dafür aber die Entwickler von etree. Der letzte Build hat die Abhängigkeit auf etree via go get github.com/beevik/etree heruntergeladen und damit ein geändertes etree Package bekommen. Denn go get lädt immer den aktuellen Stand des Master Branches eines Packages herunter. Mit diesem Stand von etree schlagen die Tests fehl.

Und jetzt? Bleiben zwei Möglichkeiten: Entweder auf einen Fix von etree warten. Oder die vorherige Version von etree verwenden, indem wir etree ins vendor-Verzeichnis (siehe unten) packen und einchecken. Mit Go 1.11 kommt eine neue, dritte Möglichkeit dazu, nämlich Go Modules.

Ein erstes Go-Modul

Also drehen wir die Zeit zurück und starten unser Go-Projekt nochmal neu, diesmal mit Go Modules. Ein Go-Modul sind ein oder mehrere Go Packages mit einer Version und Abhängigkeiten zu anderen Modulen. Jedes Modul hat einen eindeutigen Modul-Pfad. In Go 1.11 sind Modules noch experimentell und müssen mit der Umgebungsvariable GO111MODULE=on aktiviert werden.

Wir starten unser Projekt mit einem Package (github.com/red6/gomod) im GOPATH. Damit aus dem Projekt ein Go-Modul wird, müssen wir es mit go mod init initialisieren. Liegt das Package im GOPATH, übernimmt Go den Import-Pfad des Packages github.com/red6/gomod als Modul-Pfad und erzeugt die Datei go.mod. In der Datei go.mod sind der Modul-Pfad und alle Abhängigkeiten des Moduls hitnerlegt. Bis jetzt steht in der go.mod nur module github.com/red6/gomod.

Jetzt kommt die erste Abhängigkeit auf das Package github.com/gorilla/mux, wie gewohnt via go get github.com/gorilla/mux. Aber welche Version haben wir bekommen? In der go.mod steht Version 1.6.2. Aber warum? Go ermittelt die höchste Version des Packages als höchstes Tag nach semantischer Versionierung. Alternativ kann auch explizit eine Version angegeben werden (siehe Tabelle). Go hat jetzt noch eine weitere Datei go.sum erzeugt, mit einem Hash des Packages sowie Hashes aller transitiven Abhängigkeiten des Packages. Mit diesem Hash prüft go das jeder zukünftige Build für die angegebene Version des Packages auch denselben Code verwendet. Die heruntergeladenen Abhängigkeiten werden von Go zentral unter $GOPATH/pkg/mod abgelegt und nicht eingecheckt. Das Gorilla Mux Package finden wir dort unter $GOPATH/pkg/mod/github.com/gorilla/mux@v1.6.2. Jetzt holen wir uns noch das Package etree via go get github.com/beevik/etree. Die ermittelte Version 1.0.1 wird automatisch in go.mod und go.sum eingetragen. Jetzt noch die Konfigurationsdateien go.mod und go.sum einchecken und fertig ist unser erstes Go-Modul. Um Go Modules besser verstehen zu können, schauen wir uns den Weg zu ihnen näher an.

Befehl Modul Version
go get github.com/gorilla/mux@latest Lädt die aktuellste Modul Version (aktuelles Verhalten von go get).
go get github.com/gorilla/mux@master Lädt den master Branch des Moduls.
go get github.com/gorilla/mux@v1.6.2 Lädt Modul Version v1.6.2 über Versionsnummer.
go get github.com/gorilla/mux@e3702bed2 Lädt Modul Version v1.6.2 über Commit Hash.
go get github.com/gorilla/mux@c856192 Lädt einen nicht getaggten Stand über Commit Hash und bildet die Pseudo Versionsnummer v0.0.0-20180517173623-c85619274f5d.

Der Weg zu Go Modules

Am Anfang von Go war der GOPATH, das war 2012. Schnell war klar, dass der er in seiner Form nicht für Projekte ausreicht, die wiederholbare Builds über einen langen Zeitraum benötigen. Ein Build ist wiederholbar, wenn er immer dasselbe Ergebnis liefert, egal ob der Build heute ausgeführt wird oder in einem Jahr. Ein wiederholbarer Build ist nur möglich, wenn der Build auch nächstes Jahr exakt dieselben Package-Versionen verwendet wie der Build heute. Schnell gab es mit godep oder später glide Ansätze zur Verwaltung der Package-Versionen.

Vendoring

Mit Go 1.5 wurde das vendor-Verzeichnis experimentell eingeführt. Beim Build prüft Go, ob es im Projekt ein Verzeichnis vendor gibt. Ist das der Fall, haben Packages aus dem vendor-Verzeichnis Vorrang vor Packages aus dem GOPATH. So können alle Packages, die ein Projekt benötigt, ins vendor-Verzeichnis eingecheckt werden. Damit wird immer dieselbe Version der Abhängigkeiten verwendet und der Build ist unabhängig davon, ob GitHub gerade nicht erreichbar ist oder ein Autor sein Package gelöscht hat. Das vendor-Verzeichnis wurde mit Go 1.6 in den Standard übernommen. Inzwischen bauen alle Tools zur Verwaltung von Package-Abhängigkeiten auf dem vendor-Verzeichnis auf und legen die Package-Versionen dort ab, auch bei großen Projekten wie Kubernetes, Docker oder CockroachDB wird das so gehandhabt. An diesen großen Projekten zeigen sich aber auch die Nachteile: Bei CockroachDB ist das vendor-Verzeichnis 58 MB, bei Kubernetes mit 818 Package-Abhängigkeiten 209 MB groß. Bei jedem Build müssen die Packages aus dem Verzeichnis mit heruntergeladen und kompiliert werden. Änderungen der Abhängigkeiten blähen das Git Repository also stark auf.

Auch wenn das vendor-Verzeichnis wichtig ist und einige Probleme gelöst hat, erfüllt es noch nicht die Anforderungen großer Projekte mit sehr langen Laufzeiten. Aber keine Bange, das Verzeichnis wird bis zu Go 2 und vermutlich auch noch darüber hinaus unterstützt.

Gopher ante portas: Go hält Einzug auf JAXenter!

Wenige (relativ) neue Programmiersprachen erfreuen sich derzeit einer so großen Belibtheit wie Go. Googles Alternative für „C“ ist immer häufiger Zentrum interessanter Anwendungen und mittlerweile im Mainstream angekommen. Grund genug, sich ein wenig eingehender mit der Sprache zu befassen. Folgende Artikel, Kolumnen und Infografiken sind Teil unserer aktuellen Go-Aktion auf JAXenter:

Happy Gophing!

dep

Im Jahr 2017 startete die Entwicklung von dep als offizielles Experiment des Go Package Management Committees. Wenn das Experiment glückt, sollte dep die offizielle Lösung fürs Package-Management werden. Ein Erfolg war zu erwarten, denn dep orientiert sich an bestehenden Tools der Go Community wie glide und übernimmt Mechanismen anderer Package-Manager wie cargo oder bundler.

dep nutzt zwei Konfigurationsdateien, Gopkg.toml mit den Abhängigkeiten und Gopgk.lock mit den Package Hashes und Versionen. Abhängigkeiten legt dep im vendor-Verzeichnis ab. Dabei erlaubt dep sehr flexible und mächtige Angaben der Versionsnummer bei Abhängigkeiten mit Semantic Versioning als Basis. Gültige Versionsangaben in dep sind beispielsweise 1.5-1.7.0 (d.h. >= 1.5 und <= 1.7.0) und ~1.6.0 (d.h. >=1.6 und <1.7.0), um nur einige zu nennen. Die Version kann in dep also wesentlich flexibler und mächtiger angegeben werden als bei Go Modules, was nur die Angabe einer minimal benötigten Version erlaubt.

Das Go-Team hat das Experiment dep genau verfolgt, dennoch wurde es nicht in den Go-Standard übernommen, weil nicht alle davon überzeugt waren. Dann gilt der alte Go-Grundsatz: „If in doubt, leave it out“. Das Go-Team hat sich aber weiter intensiv mit dem Thema Package-Management beschäftigt, vor allem Russ Cox, von dem die wesentlichen Konzepte für Go Modules sowie die aktuelle Implementierung kommen.

Im Kern sagt Russ Cox das deps Flexibilität und Mächtigkeit für Anwender schwer überschaubar sind und in der Praxis keine besseren Ergebnisse liefern als der Ansatz der minimalen Versionsnummern von Go Modules (siehe unten). Diese Flexibilität erfordert eine sehr komplexe Implementierung des Package-Management-Tools selbst (Stichwort Version SAT), was zu Lasten von Wartbarkeit und Performance geht. Ein Vergleich von Go Modules und dep würde hier den Rahmen sprengen. Die wichtigsten Punkte sind im Twitter Thread von Russ Cox zu finden.

Aber eines ist klar: Die Zusammenarbeit von Go Community und Googles Go-Team hat nicht gut funktioniert. Wesentliche Kritikpunkte wurden zu lange nicht in Richtung dep und der Go Community kommuniziert. Auf der anderen Seite wurden die Go Modules zu sehr im stillen Kämmerchen des Googleplex vorangetrieben und die Community zu wenig involviert. Das hat auch das Go-Team eingesehen und gelobt Besserung.

Advanced Go Modules

Die Grundlagen von Go Modules haben wir kennengelernt. Jetzt gehen wir ins Detail. Bis jetzt stehen in der go.mod nur Abhängigkeiten auf andere Module mit require. Mit exclude können bestimmte Versionen anderer Module ausgeschlossen werden (siehe Listing 1). Im Notfall geht auch ein replace eines Moduls, um ein Modul durch ein anderes Modul zu ersetzen (siehe Listing 1). Mit der Angabe replace „bad/thing“ v1.3.0 => „good/thing“ v1.5.2 ersetzen wir Modul bad/thing in Version v1.3.0 durch Modul good/thing in Version v1.5.2. Bei Modulen, die direkte Abhängigkeiten sind, macht das keinen Sinn, denn dort können wir ja die require-Abhängigkeit direkt ändern. Bei indirekten Abhängigkeiten können wir nichts ändern, da ist ein replace die letzte Möglichkeit. Mit require, exclude und replace von bestimmten Modul-Versionen haben wir alle Möglichkeiten der Modul-Konfiguration kennengelernt. Warum das reicht und was intern alles passiert, werden wir noch genauer kennenlernen. Aber zunächst einen kleinen Exkurs über die Versionierung mit Go Modules.

module example.com/hello
require (
     golang.org/x/text v0.3.0
     gopkg.in/yaml.v2 v2.1.0
) 
exclude github.com/go-stack/stack v1.6.0
replace (
     github.com/go-stack/stack v1.4.0 =&gt; ../stack/
     golang.org/x/text =&gt; github.com/pkg/errors v0.8.0
)

Semantic Versioning

Go Modules bauen auf semantische Versionierung. Nach dieser besteht eine Versionsnummer aus drei Stellen: Major.Minor.Patch, zum Beispiel 1.5.2. In einer neuen Major-Version darf sich alles ändern, mit einer neuen Patch-Version kann eventuell Funktionalität hinzukommen. Die Patch-Version ist ausschließlich für Bugfixes ohne Änderungen für Nutzer vorgesehen. Änderungen von Minor- oder Patch-Version sind also immer abwärtskompatibel ohne Auswirkungen auf Nutzer. Major-Version 0 hat einen Sonderstatus, da darf sich noch immer alles ändern.

Go-Module und die Packages eines Moduls werden nach Semantic Versioning versioniert. Verschiedene Major-Versionen müssen nicht miteinander kompatibel sein. Sie verhalten sich für Go wie verschiedene Packages. Die Major-Version wird zu einem Teil des Import-Pfades eines Packages, zum Beispiel red6/payment/v2. Per Konvention werden Major-Versionen 0 und 1 weggelassen. Deswegen bleibt es in obigem Beispiel beim Import-Pfad github.com/gorilla/mux für Version 1.6.2. Für Major-Versionen gibt es in Git Unterverzeichnisse oder Branches. Ein gutes Beispiel hierfür ist das Package rsc.io/quote von Russ Cox aus einer Demo für Go Modules. Es enthält einen Branch v2 für Major-Version 2 und im Master Branch ein Unterverzeichnis v3 für Major-Version 3. Die Abhängigkeit auf v2 holen wir via go get rsc.io/quote/v2, auf Major-Version 3 mit rsc.io/quote/v3.

Ein Projekt kann verschiedene Major-Versionen eines Packages nutzen, ohne dass es Probleme gibt. Das ist für große Projekte notwendig, die eine Migration auf eine neue Major-Version über einen längeren Zeitraum strecken müssen. Zukünftig wird es auch Tools geben, die den Umstieg auf eine neue Major-Version erleichtern, indem sie überall im Code den Package Pfad von rsc.io/quote/v2 auf rsc.io/quote/v3 ändern.

Aber was ist mit Go Packages, die noch keine getaggte Version haben? Für diese erzeugt Go eine Pseudo-Versionsnummer aus Zeitstempel und Commit Hash, zum Beispiel v0.0.0-20171023230436-0bae9679d4e3.

An einer Stelle geht Go über das Versprechen der semantischen Versionierung hinaus. Go verlangt, dass Packages mit demselben Import-Pfad miteinander kompatibel bleiben. Das nennt Russ Cox die Import Compatibility Rule und ist eines der Prinzipien von Go Modules. Also wenn eine neue Modul-Version weiterhin ein Package mit demselben Import Pfad enthält, wie eine vorherige Version, so muss sich das Package auch gleich verhalten.

Minimal Version Selection

Eine wichtige Design-Entscheidung der Go Modules ist es, bei Abhängigkeiten die minimal notwendige Version des benötigten Moduls anzugeben. Und zwar nur genau diese. Wenn unser Modul das Package red6/payment in Version 1.7.0 braucht, schreiben wir 1.7.0 hinein. Jetzt wird unter Version 1.7.1 ein Bugfix von red6/payment veröffentlicht. Welche Version verwendet unser Build jetzt? Immer noch Version 1.7.0, denn so steht es in der go.mod. Um den Bugfix zu verwenden, müssen wir explizit Version 1.7.1 in die Datei eintragen. Es ist nicht möglich, die Abhängigkeit so hineinzuschreiben, dass immer die neueste Version nach Semantic Versioning verwendet wird. Das ist volle Absicht: Russ Cox nennt das Minimal Version Selection.

Jetzt machen wir es etwas komplizierter. Nehmen wir an, es gibt ein Modul red6/paypal in Version 1.0.1, das red6/payment in Version 1.7.0 braucht, und ein Modul red6/paydirekt in Version 1.3.0, das red6/payment in Version 1.6.3 braucht. Soweit noch kein Problem, beide Module können genau die jeweils angegebene Version von red6/payment verwenden, das eine Modul 1.7.0 und das andere Modul 1.6.3. Was aber, wenn wir ein neues Projekt red6/checkout starten, das sowohl red6/paypal in Version 1.0.1 als auch red6/paydirekt in Version 1.3.0 verwendet?

Der Import-Pfad von red6/payment in den Versionen 1.6.3 und 1.7.0 ist gleich, also kann der Build nicht beide Versionen verwenden, sondern muss sich für eine entscheiden. Es hilft also nichts: Go Modules kann nicht für beide Module die angegebene Version von red6/payment verwenden. Um das zu entscheiden, greift wieder Semantic Versioning. Dies führt zur Entscheidung für Version 1.7.0, denn damit müssen beide zurechtkommen. In die go.sum des neuen Projektes red6/checkout wird dann auch die Version 1.7.0 für das Package red6/payment geschrieben. Das wird dann wieder eingecheckt, damit der Build auch in Zukunft wiederholbar bleibt.

Go Modules bringen noch einige Befehle mit, um Abhängigkeiten einfacher zu verwalten. Mit go get –u können wie gewohnt alle Abhängigkeiten auf den neuesten Stand gebracht werden. Neu ist die Variante go get -u=patch, die alle Abhängigkeiten auf die neueste Patch-Version hochzieht. Dann gibt es noch go mod tidy, um nicht mehr benötigte Abhängigkeiten zu entfernen und noch ein wenig mehr (siehe go help mod tidy). Der Aufruf go mod graph gibt den Graph der Abhängigkeiten auf der Kommandozeile aus (grafische Tools werden folgen).

Wichtigste Befehle für Go Modules

go get

Mit go get –u können wie gewohnt alle Abhängigkeiten auf den neuesten Stand gebracht werden. Neu ist die Variante go get -u=patch, die alle Abhängigkeiten auf die neueste Patch Version hochzieht.

go mod init

Initialisiert ein Modul im aktuellen Verzeichnis. Liegt das aktuelle Verzeichnis bereit, wird der Modul-Pfad mit dem aktuellem Pfad im GOPATH initialisiert. Optional kann ein Modul-Pfad via go mod init red6/newmod angegeben werden.

go mod tidy

Mit diesem Befehlt wird die Modulkonfiguration wieder mit dem Quellcode abgeglichen. Nicht mehr benötigte Abhängigkeiten werden entfernt, transitive Abhängigkeiten aktualisiert und aufgeräumt.

go mod graph

Der Aufruf go mod graph gibt den Graph der Abhängigkeiten auf der Kommandozeile aus. Grafische Tools werden folgen.

Minimal Version Selection ist einfach und verständlich aber doch neu und ganz anders als Rusts cargo oder Rubys bundler. Minimal Version Selection dient dem Prinzip der Wiederholbarkeit. Das Prinzip der Wiederholbarkeit besagt, das Ergebnis des Builds einer bestimmten Version darf sich niemals ändern. Ein Build muss also immer wiederholbar sein und das gleiche Ergebnis produzieren, egal ob er heute oder in einem Jahr durchgeführt wird. Dies sorgt für Stabilität und Zuverlässigkeit bei der Entwicklung. In dieses Konzept hat das Go-Team und insbesondere Russ Cox viel Arbeit investiert. Er hat analysiert, Daten erhoben, Algorithmen verglichen und viel ausprobiert. Seine Blog-Einträge zu verschiedenen Aspekten von Go Modules sind sehr umfangreich und absolutes Muss für jeden der es genau wissen will.

Road Ahead

Bleibt noch ein letztes Prinzip von Go Modules – Kooperation. Es besagt, das wir alle zusammen das Ökosystem der Go Packages pflegen müssen. Mangelnde Kooperation der Community kann und soll nicht durch Tools umgangen werden. Wir sind die Community, also sind wir gefragt. Das Package-Ökosystem ist (neben der Sprache selbst) der wichtigste Erfolgsfaktor für Go.

Ob das Experiment der Go Modules erfolgreich wird, werden wir sehen. Ganz wichtig ist dabei das Feedback aus der Community. Du und ich müssen dem Go-Team melden, was gut funktioniert und was uns noch fehlt. Dann werden Go Modules zum Erfolg und mit Go 1.12 in den Standard übernommen. Das wäre für Go sehr wichtig, denn laut Ergebnissen des Go Surveys 2017 ist Dependency-Management eine der wichtigsten Herausforderungen für Go. Für Go 2 bleiben noch genug spannenden Themen wie Generics und Error Handling (siehe hier).

Verwandte Themen:

Geschrieben von
Jan Stamer
Jan Stamer
Jan Stamer ist Senior-Softwareentwickler bei red6 in Hamburg, einem Spin-off der Hanse-Merkur-Versicherung. red6 entwickelt innovative Insurance-Lösungen mit zeitgemäßen Technologien mit Schwerpunkt auf Java und Node.js.
Kommentare

Hinterlasse einen Kommentar

Hinterlasse den ersten Kommentar!

avatar
400
  Subscribe  
Benachrichtige mich zu: