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:

  • module ist der module path (Modulpfad). Dies ist der Import-Präfix für alle Pakete innerhalb des Moduls. Wenn das Modul von anderen Entwicklern per go get importiert werden soll, muss dieser Pfad mit der Repository-URL übereinstimmen.
  • go 1.24 ist 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 (wenn GOTOOLCHAIN=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-service verwenden. 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.mod und go.sum. Füge go.sum nicht in .gitignore ein. 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.mod reicht 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.0 zu setzen, aber vergessen, den module-Pfad zu ändern. Dann gibt go get github.com/nonla/pricing@v2.0.0 einen Fehler der Art invalid 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: replace gilt nur für das Haupt-Modul. Wenn dein Modul eine Bibliothek ist, erhalten Benutzer dein replace nicht. Außerdem solltest du replace mit lokalem Pfad nicht committen. Der Build in der CI wird fehlschlagen, weil das Verzeichnis ../pricing dort 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.sum hat keinen entsprechenden Eintrag, meist weil jemand go.mod manuell bearbeitet hat. Lösung: go mod tidy ausführen.
  • checksum mismatch: Der Modulinhalt stimmt nicht mit dem Hash überein. Versuche go clean -modcache. Falls das nicht hilft und es sich um ein öffentliches Modul handelt, ist dies ein ernstes Zeichen – lösche die Zeile nicht aus go.sum, um das Problem zu beheben.
  • go: updates to go.mod needed: Die CI läuft oft mit -mod=readonly, aber go.mod ist nicht aufgeräumt. Führe go mod tidy lokal aus und committe erneut.
  • Falsche Major-Version importiert: Du schreibst github.com/go-chi/chi statt .../chi/v5. Du bekommst entweder die alte v1 oder eine seltsame Pseudo-Version. Überprüfe den Import-Pfad zuerst.
  • Missbrauch von vendor/: go mod vendor ist noch nützlich, um vollständig offline zu bauen. Ansonsten sorgen Proxy zusammen mit go.sum bereits 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 tidy vor jedem Commit aus.
  • Nutze go.work statt replace für die Entwicklung mehrerer Module gleichzeitig.
  • Konfiguriere GOPRIVATE für deine internen Repositories.
golanggo modulesdependencyversioning