Warum manuelle Versionierung der Schmerz eines jeden Go-Entwicklers ist
\nHaben Sie sich jemals dabei ertappt zu denken: «Welche Version läuft gerade in der Produktion?» oder eine Stunde damit verbracht, herauszufinden, welcher Commit einen Fehler in einem vor einem Monat erstellten Binary verursacht hat? Wenn ja, sind Sie hier genau richtig. Die Frage «Application auto build versioning» ist eine der beliebtesten auf Stack Overflow (tausende Aufrufe und dutzende Lösungen). Und das zu Recht: In der modernen Entwicklung, wo CI/CD-Pipelines alle 10 Minuten Builds durchführen, ist das manuelle Setzen von Versionen der Weg ins Chaos.
\nIn diesem Artikel behandeln wir die automatische Versionierung von Builds in Go von A bis Z. Sie erfahren, wie Sie ein System implementieren, das selbstständig die Versionsnummer, den Commit-Hash, das Build-Datum und sogar ein Flag für ein «schmutziges» Repository setzt. Der Code funktioniert sowohl in der lokalen Entwicklung als auch in GitHub Actions, GitLab CI oder Jenkins.
\n\nDas Problem: Was ist falsch an manuellem version.go?
\nViele Anfänger erstellen eine Datei version.go und schreiben:
package main\n\nvar Version = "1.0.0"\nUnd das funktioniert genau bis zum ersten Commit. Dann beginnt es:
\n- \n
- «Ich habe vergessen, die Version vor dem Build zu aktualisieren» \n
- «In der Produktion läuft eine Version, im Code eine andere» \n
- «Welcher Commit entspricht diesem Build?» \n
Das Problem ist, dass die Version Metadaten des Builds sind, kein Teil des Quellcodes. Sie sollte automatisch während der Kompilierung generiert werden.
\n\nDie Lösung: ldflags — die Geheimwaffe von Go
\nDer Go-Compiler erlaubt es, Werte von Variablen während des Builds über das Flag -ldflags zu «ersetzen». Das ist Auto Build Versioning in seiner klassischen Form. Wir speichern die Version nicht im Code — wir übergeben sie dem Compiler.
Schritt 1: Definieren der Versionsstruktur
\nErstellen wir ein Paket version mit Variablen, die während des Builds gefüllt werden:
// version/version.go\npackage version\n\nvar (\n // Version — semantische Version (manuell oder via Git-Tag gesetzt)\n Version = "dev"\n\n // Commit — kurzer Commit-Hash\n Commit = "none"\n\n // BuildTime — Build-Zeit im ISO-8601-Format\n BuildTime = "unknown"\n\n // IsDirty — Flag, dass das Repository nicht-committete Änderungen enthält\n IsDirty = "false"\n)\n\nfunc String() string {\n return "v" + Version + " (commit: " + Commit + ", built: " + BuildTime + ")"\n}\n\nSchritt 2: Bauen mit ldflags
\nJetzt kommt der interessante Teil. Anstelle von go build verwenden wir ein Skript oder Makefile, das die benötigten Werte berechnet und an den Compiler übergibt:
#!/bin/bash\n# build.sh\n\nVERSION=$(git describe --tags --always --dirty 2>/dev/null || echo "dev")\nCOMMIT=$(git rev-parse --short HEAD 2>/dev/null || echo "none")\nBUILD_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ")\nIS_DIRTY=$(git diff --quiet || echo "true")\n\ngo build -ldflags="\\\n -X 'main/version.Version=$VERSION' \\\n -X 'main/version.Commit=$COMMIT' \\\n -X 'main/version.BuildTime=$BUILD_TIME' \\\n -X 'main/version.IsDirty=$IS_DIRTY'" \\\n -o myapp .\nWichtig: Der Pfad main/version.Version hängt von Ihrer Projektstruktur ab. Wenn das Paket version im Stammverzeichnis liegt — schreiben Sie main/version.Version. Wenn es in einem separaten Verzeichnis liegt — github.com/ihr-benutzername/projekt/version.Version.
Schritt 3: Verwendung in der Anwendung
\nJetzt kann an jeder Stelle im Programm die Version ausgegeben werden:
\n// main.go\npackage main\n\nimport (\n "fmt"\n "main/version"\n)\n\nfunc main() {\n fmt.Println("App version:", version.String())\n fmt.Println("Is dirty repo:", version.IsDirty)\n}\nBauen und starten Sie:
\n$ ./build.sh\n$ ./myapp\nApp version: v1.2.3 (commit: a1b2c3d, built: 2024-03-15T10:30:00Z)\nIs dirty repo: false\n\n