Go modules: Dependency- und Versionsverwaltung
1. Überblick
Seit Go 1.11 sind Go modules der offizielle Mechanismus zur Verwaltung von Abhängigkeiten. Seit Go 1.16 ist dies der Standardmodus und ersetzt die alte Methode auf Basis von GOPATH. Dieser Artikel führt dich durch den gesamten Lebenszyklus eines Moduls: Initialisierung, Hinzufügen von Abhängigkeiten, Auswahl und Aktualisierung von Versionen, Umgang mit Major Versions v2+ sowie Arbeit mit lokalem Code oder privaten Repositories. Nach dem Lesen wirst du verstehen, was in go.mod und go.sum enthalten ist. Du wirst auch wissen, warum Go eine bestimmte Version wählt und wie du häufige Dependency-Fehler behebst.
2. Initialisierung eines Moduls
Ein Modul ist ein Verzeichnisbaum mit einer go.mod-Datei an der Wurzel. Erstelle ein neues Modul mit go mod init:
mkdir order-service && cd order-service
go mod init github.com/nonla/order-service
Die go.mod-Datei wird erstellt:
module github.com/nonla/order-service
go 1.24
Bedeutung jeder Zeile:
moduleist der module path (Modulpfad). Dies ist der Import-Präfix für alle Pakete innerhalb des Moduls. Wenn das Modul von anderen Entwicklern pergo getimportiert werden soll, muss dieser Pfad mit der Repository-URL übereinstimmen.go 1.24ist die Mindestversion der Go-Sprache, die das Modul erfordert. Seit Go 1.21 ist diese Zeile erforderlich. Wenn deine Toolchain älter ist, wird Go automatisch die passende Toolchain herunterladen (wennGOTOOLCHAIN=auto) oder einen Fehler melden.
Hinweis: Bei Anwendungen, die intern bleiben und niemals von außen importiert werden, kannst du einen kürzeren Pfad wie
order-serviceverwenden. Es ist jedoch empfehlenswert, bereits von Anfang an einen URL-ähnlichen Pfad zu verwenden, um später keine Massen-Änderungen an den Import-Statements durchführen zu müssen.
3. Hinzufügen von Abhängigkeiten
3.1. Mit go get
Angenommen, du benötigst eine UUID-Bibliothek zur Generierung von Bestellungs-IDs. Füge die Bibliothek github.com/google/uuid hinzu:
go get github.com/google/uuid@v1.6.0
Schreibe dann Code, der die Bibliothek nutzt:
// order/order.go
package order
import "github.com/google/uuid"
type Order struct {
ID string
Customer string
Total int64 // Einheit: Dong
}
func NewOrder(customer string, total int64) Order {
return Order{
ID: uuid.NewString(),
Customer: customer,
Total: total,
}
}
go.mod hat nun einen zusätzlichen require-Block:
module github.com/nonla/order-service
go 1.24
require github.com/google/uuid v1.6.0
Du kannst auch go get überspringen: Schreibe einfach import zuerst, führe dann go mod tidy aus. Go findet automatisch die neueste Version und trägt sie in go.mod ein.
3.2. Die Datei go.sum
Gleichzeitig erstellt Go die Datei go.sum:
github.com/google/uuid v1.6.0 h1:<Hash des Modulinhalts>
github.com/google/uuid v1.6.0/go.mod h1:<Hash der go.mod-Datei>
Jede Abhängigkeit hat zwei Zeilen. Die erste ist ein Hash des gesamten Modulinhalts, die zweite ist der Hash der go.mod-Datei selbst. Wenn Go ein Modul lädt, vergleicht es den tatsächlichen Hash mit dem go.sum-Eintrag. Es prüft auch gegen die öffentliche Checksum-Datenbank (sum.golang.org). Wenn jemand eine bereits veröffentlichte Version modifiziert, schlägt der Build fehl, statt silently unerwarteten Code zu verwenden.
Hinweis: Committe immer beide Dateien:
go.modundgo.sum. Fügego.sumnicht in.gitignoreein. Wenn diese Datei fehlt, kann die CI-Pipeline die Abhängigkeiten nicht verifizieren.
3.3. Direkte und indirekte Abhängigkeiten
Füge testify zum Schreiben von Tests hinzu:
go get github.com/stretchr/testify@v1.10.0
go.mod hat nun zwei require-Blöcke:
require (
github.com/google/uuid v1.6.0
github.com/stretchr/testify v1.10.0
)
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
Der Kommentar // indirect markiert Module, die dein Code nicht direkt importiert, aber für andere Abhängigkeiten benötigt werden. Go trägt sie in go.mod ein, damit ein Rebuild vollständig reproduzierbar ist. Dadurch muss Go nicht jedes Mal die go.mod-Datei jeder Abhängigkeit neu durchlesen, um den Dependency-Graph zu konstruieren.
4. Versionierung und wie Go eine Version wählt
4.1. Semantic Versioning
Go modules verwenden Semantic Versioning in der Form vMAJOR.MINOR.PATCH:
PATCH(v1.6.0 → v1.6.1): Bugfixes, keine API-Änderungen.MINOR(v1.6.0 → v1.7.0): Neue Features, rückwärtskompatibel.MAJOR(v1 → v2): Breaking Changes.
Versionen müssen git-Tags mit Präfix v sein. Wenn du auf einen Commit zeigst, der noch kein Tag hat, erstellt Go eine Pseudo-Version:
go get github.com/google/uuid@0f3b7a2
github.com/google/uuid v1.6.1-0.20240815101530-0f3b7a2c1d4e
Eine Pseudo-Version besteht aus drei Teilen: Basisversion, UTC-Zeitstempel des Commits und die ersten 12 Zeichen des Commit-Hashes. So wird die Pseudo-Version trotzdem chronologisch korrekt sortiert.
4.2. Minimal Version Selection
Dies ist der größte Unterschied zu npm oder Maven. Go verwendet den Algorithmus Minimal Version Selection (MVS). Für jedes Modul wählt Go die höchste Version unter den angeforderten Versionen, nicht die neueste verfügbare Version.
Beispiel: Dein Projekt hat zwei Abhängigkeiten:
order-service
├── payment-sdk → require github.com/google/uuid v1.3.0
└── shipping-sdk → require github.com/google/uuid v1.5.0
Selbst wenn uuid v1.6.0 veröffentlicht wurde, wählt Go v1.5.0. Der Grund: Das ist die niedrigste Version, die alle Anforderungen erfüllt. Konsequenzen:
- Builds sind reproduzierbar. Eine neue Veröffentlichung einer Bibliothek ändert nicht automatisch deinen Build.
- Es ist keine separate Lock-Datei nötig.
go.modreicht aus, um die Versionen zu bestimmen. - Wenn du eine neuere Version verwenden möchtest, musst du dies bewusst durchführen.
Sehe dir die tatsächlich ausgewählten Versionen an:
go list -m all
github.com/nonla/order-service
github.com/davecgh/go-spew v1.1.1
github.com/google/uuid v1.5.0
...
4.3. Upgrades und Downgrades
Überprüfe, welche Abhängigkeiten neuere Versionen haben:
go list -m -u all
github.com/google/uuid v1.5.0 [v1.6.0]
github.com/stretchr/testify v1.10.0
Die Version in Klammern ist die neueste verfügbare Version. Häufig verwendete Upgrade-Befehle:
go get github.com/google/uuid@latest # neueste Version
go get github.com/google/uuid@v1.5.0 # auf eine bestimmte Version (auch möglich für Downgrades)
go get -u=patch ./... # nur PATCH für alle Abhängigkeiten upgraden
go get -u ./... # MINOR + PATCH für alle Abhängigkeiten upgraden
go get github.com/google/uuid@none # Abhängigkeit vollständig entfernen
Hinweis:
go get -u ./...upgraded auch indirekte Abhängigkeiten. Das Diff kann sehr groß sein. Bei Production-Projekten solltest du jedes Modul einzeln upgraden, Tests durchführen und erst dann committen.
5. Major Version v2 und höher
Go wendet die Regel import compatibility an: Zwei Versionen mit gleichem Import-Pfad müssen rückwärtskompatibel sein. Daher ist ab v2 die Major-Version Teil des Module-Pfads.
Beispiel mit dem Router chi:
go get github.com/go-chi/chi/v5@v5.1.0
package main
import (
"net/http"
"github.com/go-chi/chi/v5"
)
func main() {
r := chi.NewRouter()
r.Get("/orders", func(w http.ResponseWriter, _ *http.Request) {
w.Write([]byte("Bestellungsliste"))
})
http.ListenAndServe(":8080", r)
}
Das Suffix /v5 ist Teil des Import-Pfads. Der Paketname ist weiterhin chi, daher rufst du chi.NewRouter() wie gewohnt auf. Da chi/v4 und chi/v5 unterschiedliche Pfade haben, können beide Versionen in einer Binärdatei koexistieren. Dies ermöglicht dir, schrittweise Teile deines Codes zu migrieren.
Wenn du Autor einer Bibliothek bist und v2 veröffentlichen möchtest:
module github.com/nonla/pricing/v2
go 1.24
Ändere die module-Zeile zu einem Pfad mit /v2-Suffix, aktualisiere die internen Imports und hänge das Tag v2.0.0 an.
Hinweis: Ein häufiger Fehler ist, nur das Tag
v2.0.0zu setzen, aber vergessen, den module-Pfad zu ändern. Dann gibtgo get github.com/nonla/pricing@v2.0.0einen Fehler der Artinvalid version: module contains a go.mod file, so module path must match major version.
6. Nützliche go mod-Befehle
go mod tidy # füge fehlende Module hinzu, entferne ungenutzte
go mod why -m gopkg.in/yaml.v3 # warum ist dieses Modul vorhanden?
go mod graph # zeige den kompletten Dependency-Graph
go mod verify # überprüfe, ob Cache mit go.sum übereinstimmt
go mod download # lade Abhängigkeiten voraus (nützlich für Docker-Layer)
Beispielausgabe von go mod why:
# gopkg.in/yaml.v3
github.com/nonla/order-service/order
github.com/stretchr/testify/assert
gopkg.in/yaml.v3
Gelesen von oben nach unten: Das Paket order (in einem Test) importiert testify/assert, und testify/assert importiert wiederum yaml.v3.
Führe go mod tidy vor jedem Commit aus. In der CI kannst du überprüfen, ob go.mod bereits aufgeräumt ist:
go mod tidy -diff
Seit Go 1.23 zeigt das Flag -diff die Unterschiede und gibt einen Exit-Code ungleich 0 zurück, wenn die Datei nicht bereinigt ist. Der Befehl überschreibt die Datei nicht.
7. Arbeit mit lokalem Code: replace und go.work
7.1. Direktive replace
Angenommen, du debuggst die Bibliothek pricing in einem benachbarten Verzeichnis und möchtest, dass order-service sofort die geänderte Version nutzt:
require github.com/nonla/pricing v1.2.0
replace github.com/nonla/pricing => ../pricing
Go liest Code aus ../pricing statt ihn von Netzwerk zu laden. replace kann auch auf einen Fork zeigen:
replace github.com/nonla/pricing => github.com/minh/pricing v1.2.1-fix
Hinweis:
replacegilt nur für das Haupt-Modul. Wenn dein Modul eine Bibliothek ist, erhalten Benutzer deinreplacenicht. Außerdem solltest dureplacemit lokalem Pfad nicht committen. Der Build in der CI wird fehlschlagen, weil das Verzeichnis../pricingdort nicht existiert.
7.2. Workspace mit go.work
Seit Go 1.18 ist ein Workspace eine sauberere Methode für die gleichzeitige Entwicklung mehrerer Module:
go work init ./order-service ./pricing
Die Datei go.work wird erstellt:
go 1.24
use (
./order-service
./pricing
)
Wenn du im Workspace go build oder go test ausführst, nutzt jeder Import zu github.com/nonla/pricing den lokalen Code. Du musst go.mod nicht ändern. Normalerweise wird go.work nicht committed; jeder Entwickler erstellt sie nach seinen Bedürfnissen.
7.3. exclude und retract
exclude wird im Haupt-Modul verwendet, um eine bestimmte Version auszuschließen:
exclude github.com/google/uuid v1.4.0
retract wird von Bibliotheksautoren deklariert, um anzuzeigen, dass eine veröffentlichte Version nicht mehr verwendet werden sollte:
retract v1.2.0 // falsches Steuerberechnung für internationale Bestellungen
Nachdem du eine neue Version veröffentlicht hast, die diese Zeile enthält, wird go get @latest die Version v1.2.0 ignorieren. Der Befehl go list -m -u warnt auch Benutzer, die diese Version noch verwenden.
8. Private Module und Proxy
Standardmäßig lädt Go Module über GOPROXY=https://proxy.golang.org,direct und überprüft Checksummen gegen sum.golang.org. Bei internen Repositories kann der öffentliche Proxy nicht zugreifen, und du möchtest auch nicht, dass Module-Namen nach außen dringen. Deklariere dies:
go env -w GOPRIVATE=github.com/nonla/*,gitlab.nonla.internal/*
Module, die Patterns in GOPRIVATE entsprechen, werden direkt via git geladen und überspringen die Checksum-Datenbank. Du musst Git-Authentifizierung konfigurieren, zum Beispiel mit ~/.netrc oder SSH:
git config --global url."git@github.com:".insteadOf "https://github.com/"
Falls dein Unternehmen einen eigenen Proxy hat (Athens, Artifactory…), stelle ihn am Anfang von GOPROXY:
go env -w GOPROXY=https://goproxy.nonla.internal,https://proxy.golang.org,direct
9. Testen mit Abhängigkeiten
Schreibe einen Test für NewOrder mit dem testify, das du in Abschnitt 3.3 hinzugefügt hast:
// order/order_test.go
package order
import (
"testing"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestNewOrder_GeneratesValidUniqueID(t *testing.T) {
first := NewOrder("Nguyễn Văn An", 250_000)
second := NewOrder("Trần Thị Bình", 120_000)
_, err := uuid.Parse(first.ID)
require.NoError(t, err)
assert.NotEqual(t, first.ID, second.ID)
assert.Equal(t, "Nguyễn Văn An", first.Customer)
assert.EqualValues(t, 250_000, first.Total)
}
Führe den Test aus:
go test ./...
ok github.com/nonla/order-service/order 0.004s
Der Test überprüft drei Dinge: Die ID ist eine gültige UUID, zwei Bestellungen haben unterschiedliche IDs, und die restlichen Felder werden korrekt zugewiesen. require.NoError stoppt den Test sofort, wenn die ID nicht geparst werden kann. assert protokolliert Fehler und setzt fort. Beim ersten Lauf auf einer neuen Maschine wird Go alle Abhängigkeiten gemäß go.mod laden und die Hashes mit go.sum überprüfen, bevor es kompiliert.
10. Häufige Fehler
missing go.sum entry for module providing package:go.sumhat keinen entsprechenden Eintrag, meist weil jemandgo.modmanuell bearbeitet hat. Lösung:go mod tidyausführen.checksum mismatch: Der Modulinhalt stimmt nicht mit dem Hash überein. Versuchego clean -modcache. Falls das nicht hilft und es sich um ein öffentliches Modul handelt, ist dies ein ernstes Zeichen – lösche die Zeile nicht ausgo.sum, um das Problem zu beheben.go: updates to go.mod needed: Die CI läuft oft mit-mod=readonly, abergo.modist nicht aufgeräumt. Führego mod tidylokal aus und committe erneut.- Falsche Major-Version importiert: Du schreibst
github.com/go-chi/chistatt.../chi/v5. Du bekommst entweder die alte v1 oder eine seltsame Pseudo-Version. Überprüfe den Import-Pfad zuerst. - Missbrauch von
vendor/:go mod vendorist noch nützlich, um vollständig offline zu bauen. Ansonsten sorgen Proxy zusammen mitgo.sumbereits für Reproduzierbarkeit, daher musst du nicht Tausende von Vendor-Dateien committen.
11. Fazit
Go modules lösen das Dependency-Problem mit wenigen einfachen Konzepten. go.mod deklariert Anforderungen, go.sum garantiert Integrität, und Minimal Version Selection ermöglicht reproduzierbare Builds ohne separate Lock-Datei. Major Versions gehören direkt zum Import-Pfad, daher sind Breaking Changes im Code sofort sichtbar.
Im Alltag brauchst du nur vier Gewohnheiten zu merken:
- Nutze
go get module@version, um Abhängigkeiten bewusst hinzuzufügen oder zu upgraden. - Führe
go mod tidyvor jedem Commit aus. - Nutze
go.workstattreplacefür die Entwicklung mehrerer Module gleichzeitig. - Konfiguriere
GOPRIVATEfür deine internen Repositories.