Suche
Die Golumne

Dependencys in Go: Abhängigkeiten im Griff

Frank Müller

© Reneé French (CC BY 3.0 DE) ©SuS Media

In der Welt der Programmiersprachen ist Googles Spross Go ein noch recht junger Vertreter. Doch in den acht Jahren seit ihrer Vorstellung erreichte die Sprache immer mehr Entwickler. War ihr Einsatz anfangs noch auf die Cloud und Systemprogramme fokussiert, so findet man Go nun zunehmend auch in anderen Anwendungsbereichen wieder. In den bekannten Statistiken von Tiobe, GitHub, Stack Overflow, RedMonk, IEEE oder dem PYPL-Index rückt Go kontinuierlich Platz um Platz vorwärts. Ist Google damit ein guter, wenn nicht gar „der“ C-Nachfolger gelungen? Eine kurze Vorstellung der Sprache bildet den Auftakt zu unserer neuen Kolumne.

Es ist eigentlich ein alltägliches Problem – und seit Jahren in der Welt der Programmiersprachen bekannt. Vielleicht nicht gerade seit dem ersten Tag, doch über die Zeit und mit zunehmender Akzeptanz wird auch die Anzahl der verfügbaren Bibliotheken größer. Wächst das eigene Projekt, so wird auch die Wahrscheinlichkeit größer, diverse dieser externen Bibliotheken einzubinden. Selbst in meinem kleinen 1-Personen-Projekt möchte ich ungern alles selbst entwickeln. Das wäre zwar lehrreich, kostet aber im Umkehrschluss doch etwas zu viel Zeit. Also schaue ich mich auf dem Markt der verfügbaren Lösungen um und frage vielleicht bei Twitter, Slack, Stack Overflow oder den guten alten Newsgroups. So finde ich dann die Bibliotheken, die mich in meinem Vorhaben unterstützen.

Mit etwas Glück fahre ich zuerst mit den jeweils neuesten Versionen aus den entsprechenden Repositorys auch noch gut. Doch irgendwann kommt es garantiert zu einem Fix mit unerwarteten Nebeneffekten, zu inkompatiblen Änderungen im Code oder zur genialen neuen Version, die jedoch nicht in das eigene Projekt passt. Mit der Strategie, immer auf die neueste Version zurückzugreifen, sind also Anpassungen an meinem Code notwendig. Daher möchte ich an dieser Stelle gerne für jede Version meines Builds festlegen, welche exakten Versionen der verschiedenen Bibliotheken ich einbinden möchte. Beispielsweise nutze ich die Bibliotheken Foo v1.0, Bar v2.1 und Baz v1.5 für die Version 1 meines Projekts. Aktuell nutzen die beiden Bibliotheken Bar und Baz ebenfalls die Version 1 der Bibliothek Foo. Soweit liegt kein Konflikt vor, doch ich erwarte von meinem Build-Management, diese Zusammenstellung an Versionen immer abbilden zu können.

Für Go ist das nicht anders. Die Sprache adressiert externe Bibliotheken ganz praktisch über den Host und den Pfad Ihres Repositories. So wird mit import github.com/tideland/gotogether/actor das Package actor aus der Tideland-GoTogether-Bibliothek von GitHub importiert. Mit go get <Bibliothek> können Bibliotheken explizit in das eigene Source-Verzeichnis geladen werden, ansonsten geschieht dies implizit beim Build. Die dabei herangezogene Version ist immer HEAD des Default-Branches.

Dieses unkomplizierte Vorgehen war gerade in der Anfangszeit sehr praktisch. Kein externes Werkzeug, alles sauber in die Toolchain von Go integriert. So wurde eine einfache Evaluierung externer Bibliotheken unterstützt, was seinen Anteil zur schnellen Adaption der Sprache beigetragen hat. Doch ergibt sich mit diesem Verfahren auch die Problematik nicht reproduzierbarer Builds. Von einem Tag auf den nächsten kann das eigene Projekt durch Änderungen an einer der genutzten Bibliotheken ins Straucheln geraten. Mit etwas Glück zieht dies nur überschaubare Anpassungen am eigenen Code nach sich. Doch eine neue Hauptversion mit einem stark geänderten API trifft mich mit voller Breitseite und kann so immense Auswirkungen auf mein Projekt haben.

DevOpsCon Whitepaper 2018

Free: BRAND NEW DevOps Whitepaper 2018

Learn about Containers,Continuous Delivery, DevOps Culture, Cloud Platforms & Security with articles by experts like Michiel Rook, Christoph Engelbert, Scott Sanders and many more.

Daher entstand auch in der Go-Welt bereits früh der Wunsch, für kompatible Builds durch präzise definierte Abhängigkeiten zu sorgen. Eine Lösung war der Service gopkg.in von Gustavo Niemeyer. Er konnte Importe umleiten und orientierte sich an Branches oder Tags. Wurde ein Package mit dem Namen gopkg.in/themue/foo.v2 importiert, führte das letztendlich zum Paket github.com/themue/foo mit einem der Tags v2, v2.N oder v2.N.M. Für das korrekte Tagging war der Entwickler verantwortlich, Hauptversionen mussten zueinander abwärtskompatibel bleiben. Diese Form der semantischen Versionierung für die Tags wurde nach kurzer Diskussion auch allgemein akzeptiert. Gleichzeitig veröffentlichte Dave Cheney das Build Tool gb für reproduzierbare Builds. Es basiert auf dem Vendoring, bei dem alle notwendigen Projekte im vendor-Verzeichnis innerhalb der eigenen Quellen und anschließend im eigenen Repository mit abgelegt werden. gb sorgt beim Build und auch bei Tests dafür, dass diese Bibliotheken herangezogen werden. Eine Änderung der Quellen ist hierfür nicht notwendig. Das Vendoring zog dann mit Version 1.5 auch experimentell in das Go-Toolset ein und gehörte ab Version 1.6 zum Standard.

Einen initial anderen Weg ging godep von Keith Rarick. Es analysierte die installierten Abhängigkeiten und generierte hieraus die Datei Godeps.json. Nur diese musste mit im Projekt-Repository abgelegt werden. Ein godep restore hat die benötigten Bibliotheken wieder installiert, weitere Unterkommandos standen zur Verwaltung der Abhängigkeiten zur Verfügung. Ab Go 1.6 hat dann auch godep das Vendoring unterstützt. Inzwischen ist godep allerdings abgekündigt, denn seit Go 1.9 ist das verbesserte dep das Mittel der Wahl. Es nutzt das vendor-Verzeichnis und bietet die flexible Datei Gopkg.toml für die Definition der verschiedenen Abhängigkeiten. Hier können Bedingungen auf Basis exakter semantischer Versionen, von Bereichen von Versionen, Tags, Branches und Revisionen angegeben werden. Auch alternative Pfade für Forks einer Bibliothek sind möglich. Damit ist dep bis dato der leistungsfähigste Weg zur Sicherstellung reproduzierbarer Builds.

Dann ist doch alles gegessen?

Nein, leider nicht. Eine ganz einfache Problematik wird auf diesem Weg nicht gelöst. In meinem Anfangsbeispiel sagte ich, die Version 1 meines Projekts benötigt jeweils die Versionen Foo v1.0, Bar v2.1 und Baz v1.5. War ich hier noch ganz glücklich, änderte sich dies mit Version 1.1 meines Projekts. Mit diesem neuen Release benötige ich nämlich Foo v2.0 sowie zusätzlich Yadda v2.0. Diese neue Bibliothek fühlt sich auch mit der neuen Version von Foo zufrieden, nur mit Baz leider nicht. Dieses hat inzwischen Version 1.8 erreicht, mit der mein Projekt dank Abwärtskompatibilität gut zurechtkommt. Leider stützt sie sich weiterhin auf einer 1er-Version von Foo ab, ich habe meinen Konflikt.

Abb. 1: Abhängigkeiten des fiktiven Projekts (Version 1.1)

Abb. 1: Abhängigkeiten des fiktiven Projekts (Version 1.1)

Mit diesem generellen Problem der konkurrierenden Abhängigkeiten unterschiedlicher Bibliotheken im eigenen Projekt hat sich Russ Cox vom Go-Team befasst. Beginnend mit einem Blogeintrag im Februar hat er sich systematisch mit der Thematik auseinandergesetzt und sein Konzept des Go Versioning, kurz vgo ausgearbeitet. Große Teile dieser Idee basieren auf den Erfahrungen mit dep und den anderen Tools. Warum sollte Russ auch Bewährtes wegwerfen? So gibt es eine Definitionsdatei namens go.mod für die Abhängigkeiten des eigenen Projekts wie auch eine Art Vendoring-Verzeichnis. Dieses heißt nun jedoch nurmehr v und befindet sich direkt im Source-Verzeichnis und nicht mehr innerhalb des eigenen Projekts. Hier sammelt vgo von den individuellen Bibliotheken, die in der eigenen Umgebung zum Einsatz kommen, alle entsprechenden benötigten Versionen in entsprechend benannten Unterverzeichnissen. In einem Verzeichnis src/v/github.com/tideland/gotogether befindet sich dann unter anderem ein gotogether@v1.1.0 für Version 1.1.0.

Diese Schritte sind also im Format etwas anders, in der Speicherung der Packages globaler. So können auf dem eigenen System sowohl mehrere Projekte parallel als auch genutzte externe Bibliotheken mit unterschiedlichen Versionen betrieben werden. Doch halt! Damit bleibt das Problem bei den Builds noch bestehen. Es ist zwar schön, dass die Quellen aller notwendigen Versionen vorgehalten und ihre Abhängigkeiten in einem einheitlichen Format verwaltet werden. Doch wird eine Software kompiliert, kann es weiterhin zu Konflikten beim gleichzeitigen Einsatz inkompatibler Versionen kommen. Daher führt vgo zwei weitere sehr wichtige Regeln und Techniken ein.

Die erste ist hierbei prinzipiell nur eine Konvention, die jedoch das Konzept von Repository und Pfad beim Import sicher aufgreift. Sie besagt, dass neue Versionen mit dem gleichen Importpfad kompatibel mit der vorherigen Version sein müssen. Bei Inkompatibilität muss sich hingegen der Pfad unterscheiden. Der Trick hierfür ist das Einfügen der Hauptversion der semantischen Version in den Pfad. Kann ich bei den Versionen v0 und v1 noch darauf verzichten, so soll ich ab Version 2 mit dem neuen API den Code in ein entsprechendes Unterverzeichnis packen. Meine oben bereits genannte Bibliothek Tideland GoTogether bekommt also eventuell eines Tages einen Ableger unter github.com/tideland/gotogether/v2/…. Bis dahin werden aber jeder Fix und jede Erweiterung im aktuellen Pfad bleiben.

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!

Bleibt aber noch das Problem unterschiedlicher Unterversionen durch verschiedene Bibliotheken. Ich hatte oben die Bibliothek Baz v1.8 genannt, die zum Beispiel Foo v1.5 einbindet. Nun nehme ich Bar v2.1 von oben ebenfalls hinzu. Sie soll hier im Beispiel das gute alte Foo v1.0 nutzen. Auf Basis der unterschiedlichen Pfade ist die Nutzung einer Version 1.x gemeinsam mit einer Version 2.x kein Problem. Doch wie sieht das nun für zwei verschiedene 1.x-Versionen aus? Hier sind sich fast alle Tools einig und ziehen die neueste Version heran, sie muss ja besser sein und soll die Kompatibilität per Definition erhalten haben. Doch Russ Cox ist anderer Meinung. Die Bedeutung der neuesten Version kann durch externe Einflüsse, zum Beispiel eigene Abhängigkeiten von anderen Bibliotheken, beeinflusst sein. Gleichzeitig beschreibt er schön, wie ich mir dann als Entwickler Mühe gebe, dem Build-Management mitzuteilen, dass es doch nicht diese neueste Version nutzen soll.

Sein Weg heißt daher Minimal Version Selection, kurz MVS. Hier wird im Gegensatz zu den anderen Tools die älteste, also sozusagen die kleinste erlaubte Version genutzt. In meiner go.mod gebe ich mit require an, welche Versionen einer Bibliothek ich mindestens benötige. Dieser Graph wird nun auch für die gesamte Hierarchie der referenzierten Bibliotheken aufgelöst. Nutzt nun wie oben angegeben mein Projekt Foo v1.0 und Foo v1.5, dann wird Version 1.5 als die kleinste 1.x-Version herangezogen. Auch wenn es inzwischen vielleicht bereits Foo v1.9 gibt, sind alle mit Version 1.5 zufrieden, und das wird über die gesamte Lebensdauer so bleiben. Eine nachträgliche Änderung der Version 1.5 ist nämlich sehr unwahrscheinlich. Dahingegen würde bei der Nutzung der jeweils neuesten Version die Freigabe einer Foo v1.10 diese herunterladen, was wiederum zu einem möglichen Bruch im Build führen könnte.

Und nun?

Die grundsätzlichen Ideen von vgo sind nicht sehr umfangreich:

  • Ein neues, einfaches Format zur Definition der Abhängigkeiten mit go.mod,
  • die Definition von Hauptversionen als Pfadbestandteil,
  • die Algorithmen des MVS als Weg, die stabilste Unterversion für eine Bibliothek heranzuziehen,
  • ein intelligentes Caching der Quellversionen und ihrer Definitionen sowie
  • ein Toolset, das die Verwaltung und damit die Builds erlaubt.

Und um das für jeden gut nachvollziehbar zu dokumentieren, schrieb Russ Cox eine sehr detaillierte siebenteilige Blogreihe. Mit seiner Idee der Minimal Version Selection taten sich einige zuerst schwer. Doch mit der Diskussion sowie weiteren Blogeinträgen Dritter stieg die Akzeptanz dieses Entwurfs. Sehr viele haben nun den Eindruck, dass auf dieser Basis zukünftig ein intelligentes und stabiles Build-Management möglich ist, so auch der Autor dieser Zeilen. vgo wurde akzeptiert, mit Go 1.11 werden sich bereits bald erste Bestandteile im Go Toolset wiederfinden und mit Go 1.12 wird es voraussichtlich vollständig integriert.

Verwandte Themen:

Geschrieben von
Frank Müller
Frank Müller
Der Oldenburger Frank Müller ist seit über dreißig Jahren in der IT zu Hause und im Netz vielfach als @themue anzutreffen. Das Interesse an Googles Sprache Go begann 2009 und führte inzwischen zu einem Buch sowie mehreren Artikeln und Vorträgen zum Thema.
Kommentare

Hinterlasse einen Kommentar

Hinterlasse den ersten Kommentar!

avatar
400
  Subscribe  
Benachrichtige mich zu: