Das Kompatibilitätsversprechen von Go/Golang
deckt hauptsächlich die Kernsprache und Standardbibliothek ab. Die
Versionierung externer Abhängigkeiten, die über import
-Anweisungen eingebunden
werden, unterliegen der Versionierung der jeweiligen Bibliothek und fallen
strenggenommen nicht unter dieses Versprechen. Eine Aktualisierung kann
dementsprechend zu sogenannten Breaking Changes führen, bei denen der eigene
Code angepasst werden muss.
Zum Glück orientieren sich allerdings die meisten Bibliotheken ebenfalls am Kompatibilitätsversprechen des Go-Teams und setzen auf Semantic-Versioning und der Kompatibilität ihrer API.
Dieser Artikel gibt Einblicke in die Funktionsweise von Go und zeigt dir, wie du diese Erkenntnisse in deine eigenen Projekte, sowohl Bibliotheken als auch Anwendungen, integrieren kannst.
Go-Programme, die für eine bestimmte Version der Sprache geschrieben wurden, sollen durch das Kompatibilitätsversprechen ohne Anpassung auch mit späteren Versionen der Sprache weiterhin funktionieren.
Mit der Einführung von Go-Modulen in Go 1.11 verwalten Anwendungen und
Bibliotheken ihre Abhängigkeiten explizit in einer go.mod
-Datei. Diese Datei
enthält neben den Abhängigkeiten auch den eindeutigen Modulnamen.
Dieser Modulname wird als Präfix in import
-Anweisungen dazu verwendet, um
Pakete dieses Moduls zu importieren. Der Name setzt sich dabei aus zwei
Komponenten zusammen:
Folgende Datei zeigt als Beispiel eine go.mod
Datei einer Bibliothek ohne
Abhängigkeit:
module github.com/sourcefellows/mylib
go 1.23.6
Soll diese Bibliothek in eine Anwendung eingebunden werden, kann diese über ein
entsprechendes import
-Statement eingebunden werden:
import "github.com/sourcefellows/mylib"
Die Implementierung der Bibliothek sieht so aus:
// Verbesserungsfähiges Beispiel!!
// bessere Version kommt weiter unten
package mylib
import "regexp"
func HideDigits(text string) (string, error) {
compile, err := regexp.Compile("\\d")
if err != nil {
return "", err
}
return compile.ReplaceAllLiteralString(text, "X"), nil
}
In der Anwendung sieht das Ganze dann so aus:
package main
import (
"fmt"
"log"
"github.com/sourcefellows/mylib"
)
func main() {
result, err := mylib.HideDigits("meine12345Nummer")
if err != nil {
log.Fatal(err)
}
fmt.Println(result)
}
In der obigen Anwendung bzw. der Bibliothek sieht man wahrscheinlich recht schnell, dass der Code nicht ideal ist und bei jedem Aufruf der Bibliothek der reguläre Ausdruck erneut kompiliert wird.
Durch ein kleines Refactoring kann dieses Problem einfach gelöst werden:
// Noch nicht perfekt, aber besser.
package mylib
import "regexp"
var compile = regexp.MustCompile("\\d")
func HideDigits(text string) string {
return compile.ReplaceAllLiteralString(text, "X")
}
Allerdings hat sich mit dieser kleinen Änderung ein weitaus größeres Problem eingeschlichen. Die API der Bibliothek ist nicht mehr kompatibel, da sich in diesem Zuge auch die Signatur der Funktion verändert hat.
Statt einem String und einem Fehler-Objekt, liefert die Funktion ab sofort nur noch einen String-Wert zurück. Ein Breaking Change!
Mit der oben beschriebenen inkompatiblen Änderung wird das Kompatibilitätsversprechen gebrochen. Für Packages besagt es, dass:
Packages dürfen nur API-kompatibel und abwärtskompatibel geändert werden.
Das heißt, solange das import
-Statement gleich bleibt muss ein Update der
Bibliothek abwärtskompatibel sein.
Open-Close-Principle - Das Open-Close-Principle besagt, dass Softwareentitäten (wie Klassen, Module oder Funktionen) offen für Erweiterungen, aber geschlossen für Modifikationen sein sollten.
Im Beispiel hätte man die API erweitern können und eine neue Funktion hinzufügen können:
//alte Implementierung
func HideDigits(text string) (string, error) {
...
}
//neue Implementierung
func MustHideDigits(text string) (string) {
...
}
Somit hätten alte Clients der Bibliothek ohne Anpassung weiter eingesetzt werden können und neue Clients hätten die neue API verwenden können.
Möchte man stattdessen nur noch die neue Version der API anbieten, kann auch das Bibliotheks-Modul als Ganzes versioniert und aktualisiert werden.
Wie bereits erwähnt kann der Modulname eine optionale Version enthalten. Ein
Update würde innerhalb der go.mod
dann so aussehen:
module github.com/sourcefellows/mylib/v2
go 1.23.6
Wichtig ist hier die Angabe v2 am Ende des Modulnamens.
Innerhalb der aufrufenden Anwendung kann jetzt durch einen Update des
import
-Statements die neue Version eingesetzt werden. Wird das
import
-Statement nicht angepasst, wird weiterhin die alte Version verwendet.
package main
import (
"fmt"
"github.com/sourcefellows/mylib/v2"
)
func main() {
result := mylib.HideDigits("meine12345Nummer")
fmt.Println(result)
}
Jede Anwendung kann nun explizit entscheiden mit welcher Version gearbeitet werden soll. Und was auch funktioniert…
Die Bibliotheksversionen werden bei Go als Git Tags im Repository abgelegt. Für das Beispiel ist für Version 1 das Tag
v1.0.0
angelegt. Version zwei gibt es entweder immain
Branch oder unterv2.0.0
.Das komplette Beispiel kann auch in GitHub gefunden werden. MyLib und MyLibApplication
In Go ist es ebenfalls möglich zwei Versionen einer Bibliothek gleichzeitig zu nutzen:
Die eindeutigen
import
-Statements machen das möglich!
package main
import (
"fmt"
"log"
"github.com/sourcefellows/mylib"
newver "github.com/sourcefellows/mylib/v2"
)
func main() {
result, err := mylib.HideDigits("meine12345Nummer")
if err != nil {
log.Fatal(err)
}
fmt.Println(result)
result = newver.HideDigits("123")
fmt.Println(result)
}
Das komplette Beispiel kann auch in GitHub gefunden werden. MyLib und MyLibApplication
Eigene Bibliotheken sollten immer nach dem semantic Versioniing Prinzip versioniert werden. Breaking changes der API sollten immer zu einer neuen MAJOR Version führen und in Go dementsprechend zu einem Modul-Update. So bricht kein Code bei alten Clients und diese können weiter arbeiten.
Ein Tool was hier übrigens super hilft ist
mod
. Damit lässt sich die Version von
Bibliotheken sehr einfach verwalten und aktualisieren. Die Installation läuft
über:
go install github.com/marwan-at-work/mod/cmd/mod@latest
Danach kann man eigene Bibliotheken mit einem einzigen Kommando aktualisieren
bzw. ein “Versionsupdate” durchführen. Also den Modulnamen anpassen und dazu
alle import
-Statements im Code entsprechend anpassen.
mod upgrade
Viel Spaß beim aktualisieren!
05.02.2025
Der Author auf LinkedIn: Kristian Köhler und Mastodon: @kkoehler@mastodontech.de
Der praktische Soforteinstieg für Developer und Softwarearchitekten, die direkt mit Go produktiv werden wollen.
zur Buchseite beim Rheinwerk Verlag Rheinwerk Computing, ISBN 978-3-8362-7559-0 (als PDF, EPUB, MOBI und Papier)
Source Fellows GmbH
Lerchenstraße 31
72762 Reutlingen
Telefon: (0049) 07121 6969 802
E-Mail: info@source-fellows.com