go-tutorial

Toolchain und Module

Nach diesem Kapitel kennst du die Werkzeuge, die mit Go ausgeliefert werden, kannst ein Modul anlegen, Abhängigkeiten verwalten und auf Schwachstellen prüfen – und du verstehst, was in go.mod und go.sum steht und warum Go Versionen anders auflöst als npm oder NuGet.

upwatch in diesem Teil: go.mod, go.sum und das Makefile Voraussetzungen: Teil 01

Ein Befehl für alles

In TypeScript-Projekten setzt du die Werkzeugkette aus Einzelteilen zusammen: npm oder pnpm für Pakete, tsc zum Übersetzen, ESLint, Prettier, Jest oder Vitest, dazu je eine Konfigurationsdatei. In .NET bündelt die dotnet-CLI das meiste davon; Go geht denselben Weg noch etwas konsequenter. Der Befehl go übersetzt, testet, formatiert, prüft, verwaltet Abhängigkeiten, zeigt Dokumentation an und lädt sogar neue Go-Versionen nach. Konfigurationsdateien für diese Werkzeuge gibt es nicht.

go version
go version go1.27.1 darwin/arm64

Die Befehle, die du täglich brauchst:

Befehl Zweck Entspricht etwa
go run . übersetzen und starten dotnet run, tsx main.ts
go build Binary bauen dotnet build/publish, tsc
go test ./... alle Tests ausführen dotnet test, jest
go fmt ./... Code formatieren dotnet format, prettier --write
go vet ./... statische Analyse Roslyn-Analyzer, eslint
go doc Dokumentation im Terminal –
go mod tidy go.mod/go.sum aufräumen npm prune + npm install
go get Abhängigkeit hinzufügen/ändern dotnet add package, npm install x

./... ist ein Paketmuster und bedeutet „dieses Verzeichnis und alle Unterverzeichnisse“. Fast jeder go-Befehl versteht es.

Ein Modul anlegen

Ein Modul ist eine Sammlung von Paketen mit gemeinsamer Version – vergleichbar mit einem npm-Paket oder einer .NET-Solution mit einem NuGet-Paket. Es wird durch die Datei go.mod im Wurzelverzeichnis definiert. In einem leeren Verzeichnis:

mkdir hello && cd hello
go mod init example.com/hello
go: creating new go.mod: module example.com/hello

Die erzeugte go.mod:

module example.com/hello

go 1.27.1

Das Argument von go mod init ist der Modulpfad. Er ist der Präfix aller Importpfade in diesem Modul und sollte dort liegen, wo der Code später zu finden ist, typischerweise github.com/<user>/<repo>. Der Pfad ist kein Paketname wie bei npm, sondern eine Adresse: Wenn jemand github.com/casoon/go-tutorial importiert, weiß die Toolchain, wo sie den Code findet. Für Code, der nie veröffentlicht wird, reicht ein beliebiger Pfad mit Punkt im ersten Element, etwa example.com/hello.

Lege daneben eine main.go an:

package main

import "fmt"

func main() {
	fmt.Println("hello, upwatch")
}
go run .

go run . übersetzt das Paket im aktuellen Verzeichnis in ein temporäres Binary und führt es aus. go build legt das Binary dagegen dauerhaft ab, hier als ./hello. Der Name kommt vom letzten Element des Modulpfads.

Das Beispiel im Repository

Im Tutorial-Repository liegt das Beispiel unter beispiele/02-toolchain-und-module/hello. Es hat keine eigene go.mod, sondern gehört zum Modul des Repositorys – ein Modul kann beliebig viele Pakete enthalten, und jedes Verzeichnis ist genau ein Paket.

func main() {
	fmt.Println(greeting(os.Args[1:]))
}

// greeting builds the message for the given URLs.
func greeting(urls []string) string {
	if len(urls) == 0 {
		return "upwatch: no URLs to watch yet"
	}
	return "upwatch: watching " + strings.Join(urls, ", ")
}
go run ./beispiele/02-toolchain-und-module/hello https://example.com
upwatch: watching https://example.com

Daneben liegt main_test.go. Tests stehen in Go im selben Verzeichnis wie der Code, in Dateien mit der Endung _test.go. Sie werden beim normalen Build ignoriert.

go test ./beispiele/02-toolchain-und-module/...
ok  	github.com/casoon/go-tutorial/beispiele/02-toolchain-und-module/hello	0.232s

Mehr zu Tests in Teil 11.

Formatieren, prüfen, nachschlagen

go fmt formatiert Code nach dem einen Go-Stil: Tabs zur Einrückung, feste Regeln für Leerzeichen und Klammern. Es gibt keine Optionen. Das klingt nach Bevormundung und ist in der Praxis eine Erleichterung: Diskussionen über Formatierung finden nicht statt, Diffs enthalten nur inhaltliche Änderungen, und jeder Go-Code sieht gleich aus. Der Befehl gofmt -l . listet nur die Dateien auf, die nicht formatiert sind – praktisch in CI. Dein Editor ruft gofmt (über gopls, den Language Server) normalerweise beim Speichern auf.

go vet findet Code, der kompiliert, aber fast sicher falsch ist: Printf mit falschen Platzhaltern, kopierte Mutexe, unerreichbarer Code, falsch geschriebene Struct-Tags, ein vergessenes cancel beim Context. go test führt eine Auswahl dieser Prüfungen automatisch mit aus. Für mehr Analysen nutzen viele Projekte zusätzlich staticcheck oder golangci-lint; für den Anfang genügt go vet.

go doc zeigt die Dokumentation eines Pakets, Typs oder einer Funktion im Terminal – auch für deinen eigenen Code:

go doc strings.Cut
package strings // import "strings"

func Cut(s, sep string) (before, after string, found bool)
    Cut slices s around the first instance of sep, returning the text before and
    after sep. The found result reports whether sep appears in s. If sep does
    not appear in s, cut returns s, "", false.
go doc ./internal/monitor
go doc ./internal/monitor.NewTarget

Dokumentation ist in Go einfach ein Kommentar direkt über der Deklaration, der mit deren Namen beginnt. Es gibt kein /// mit XML wie in C# und keine JSDoc-Tags. Im Browser findest du dieselben Texte für jedes öffentliche Modul auf pkg.go.dev.

Jetzt du: was go vet findet

Lege außerhalb des Repositorys ein Wegwerf-Modul an und schreibe einen Fehler hinein, den der Compiler nicht bemerkt:

mkdir vetdemo && cd vetdemo
go mod init example.com/vetdemo
cat > main.go <<'GO'
package main

import "fmt"

func main() {
	fmt.Printf("checked %d targets\n", "three")
}
GO
go build && echo "build ok"
go vet
build ok
main.go:6:22: fmt.Printf format %d has arg "three" of wrong type string

go build ist zufrieden, denn Printf nimmt beliebige Argumente. go vet kennt die Bedeutung der Platzhalter und meldet den Widerspruch mit Exit-Code 1. Ersetze %d durch %s und führe go vet erneut aus: Es bleibt still. Dieselbe Prüfung läuft übrigens automatisch, wenn du go test aufrufst.

Abhängigkeiten: go get und go mod tidy

Eine Abhängigkeit fügst du hinzu, indem du sie im Code importierst und dann go mod tidy ausführst, oder ausdrücklich mit go get:

go get modernc.org/sqlite@v1.60.1

go get ändert go.mod und go.sum, installiert aber nichts global und legt kein Verzeichnis im Projekt an. Heruntergeladene Module landen im Modul-Cache (go env GOMODCACHE, meist ~/go/pkg/mod), den sich alle Projekte teilen. Ein node_modules pro Projekt gibt es nicht.

go mod tidy gleicht go.mod mit dem Code ab: Es fügt fehlende Abhängigkeiten hinzu, entfernt nicht mehr importierte und ergänzt go.sum. Führe es nach jeder Änderung an Imports aus. Wenn go build meldet, dass ein Paket fehlt, ist go mod tidy fast immer die Lösung.

Weitere nützliche Varianten:

go get -u ./...                 # alle Abhängigkeiten auf das neueste Minor/Patch
go get example.com/lib@none     # Abhängigkeit entfernen
go list -m -u all               # zeigen, wofür es Updates gibt
go mod why -m golang.org/x/sys  # warum ist dieses Modul im Build?

Auf bekannte Schwachstellen prüfst du mit govulncheck, das du ohne Installation über go run startest:

go run golang.org/x/vuln/cmd/govulncheck@latest ./...

Anders als npm audit meldet govulncheck in erster Linie Schwachstellen in Funktionen, die dein Code tatsächlich aufruft, und zeigt den Aufrufpfad dorthin. Weil Go Abhängigkeiten nie von selbst aktualisiert (siehe Minimal Version Selection weiter unten), gehört der Befehl in den regelmäßigen Ablauf; einen echten Lauf gegen upwatch und was daraus folgt, zeigt Teil 13.

go.mod in fünf Zeilen

Damit hast du alles, was du für die nächsten Kapitel brauchst. Die go.mod von upwatch, auf das Wesentliche gekürzt:

module github.com/casoon/go-tutorial  // module path: prefix of every import in this repo
go 1.26.0                             // minimum Go version, also selects language semantics
require modernc.org/sqlite v1.60.1    // direct dependency, a minimum version, not a range
require ( … // indirect )             // what the dependencies need, maintained by go mod tidy
toolchain …                           // optional: preferred Go version (not used by upwatch)

Daneben liegt go.sum mit Prüfsummen für jedes Modul. Beide Dateien gehören ins Repository, beide pflegt die Toolchain, und ein separates Lockfile wie package-lock.json gibt es nicht. Warum das genügt, erklärt der folgende Abschnitt.

Für später: Module im Detail

Dieser Abschnitt erklärt, was unter der Haube passiert: wie Go Versionen auswählt, woher die Module kommen und wie sich die Toolchain selbst aktualisiert. Für die folgenden Kapitel brauchst du ihn nicht. Lies ihn, wenn du zum ersten Mal ein Update einspielst oder ein privates Modul einbinden willst.

Die vollständige go.mod von upwatch
module github.com/casoon/go-tutorial

go 1.26.0

require modernc.org/sqlite v1.60.1

require (
	github.com/dustin/go-humanize v1.0.1 // indirect
	github.com/google/uuid v1.6.0 // indirect
	github.com/mattn/go-isatty v0.0.24 // indirect
	github.com/ncruces/go-strftime v1.0.0 // indirect
	github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
	golang.org/x/sys v0.48.0 // indirect
	modernc.org/libc v1.77.1 // indirect
	modernc.org/mathutil v1.7.1 // indirect
	modernc.org/memory v1.12.1 // indirect
)

go 1.26.0 legt auch fest, welche Sprachversion der Compiler verwendet. Ein Beispiel: Seit Go 1.22 bekommt jede Iteration einer for-Schleife eine eigene Schleifenvariable. Ein Modul mit go 1.21 in der go.mod behält das alte Verhalten, auch wenn es mit Go 1.27 übersetzt wird. Go bricht so niemals still die Semantik älteren Codes.

require mit // indirect: Diese Module importiert nicht upwatch, sondern modernc.org/sqlite. Seit Go 1.17 stehen alle Module, die für den Build der eigenen Pakete gebraucht werden, explizit in go.mod. Das macht die Datei länger, aber die Toolchain muss nicht mehr den gesamten Abhängigkeitsgraph laden, um zu bauen. Warum github.com/google/uuid dabei ist, verrät go mod why:

go mod why github.com/google/uuid
# github.com/google/uuid
github.com/casoon/go-tutorial/internal/sqlitestore
modernc.org/sqlite
modernc.org/libc
github.com/google/uuid

Die Version v0.0.0-20230129092748-24d4a6f8daec ist eine Pseudo-Version: Das Modul bigfft hat keine Release-Tags, also verweist Go auf einen Commit mit Zeitstempel und Hash.

Module ab Version 2 tragen die Hauptversion im Pfad, etwa github.com/jackc/pgx/v5. Das ist die Import-Kompatibilitätsregel: Ein Importpfad muss über alle Versionen kompatibel bleiben; ein Breaking Change bekommt einen neuen Pfad. Dadurch können v4 und v5 derselben Bibliothek sogar gleichzeitig im Build vorkommen.

go.sum: Prüfsummen statt Lockfile

go.sum enthält für jedes Modul im Graph kryptografische Prüfsummen:

modernc.org/sqlite v1.60.1 h1:/blz53O951KWFOso4QQvEs/Fq6cDBKLtMVrYNSeJVKw=
modernc.org/sqlite v1.60.1/go.mod h1:1dIoEagfDE72QytD5scH1lxARtaUgKgHC/NuApA27r0=

Die erste Zeile ist der Hash des gesamten Modulinhalts, die zweite der Hash nur seiner go.mod. Beim Download prüft die Toolchain jedes Modul gegen diese Hashes. Hat jemand eine veröffentlichte Version nachträglich verändert, schlägt der Build fehl.

Der wichtige Unterschied zu package-lock.json oder packages.lock.json: go.sum bestimmt nicht, welche Versionen verwendet werden. Das tut allein go.mod. go.sum stellt nur sicher, dass die Versionen, die go.mod festlegt, unverändert sind.

Minimal Version Selection

npm und NuGet lösen Versionsbereiche auf. "lodash": "^4.17.0" heißt „irgendeine 4.x ab 4.17.0“, und welche es wird, hängt davon ab, was zum Zeitpunkt der Installation veröffentlicht ist. Damit zwei Entwickler dasselbe bekommen, braucht es ein Lockfile.

Go verwendet Minimal Version Selection (MVS). Jede require-Zeile ist eine Mindestversion, kein Bereich. Verlangt upwatch golang.org/x/sys v0.48.0 und eine Abhängigkeit v0.45.0, wählt Go die höchste der geforderten Mindestversionen, also v0.48.0 – und nicht die neueste, die es gerade gibt. Das Ergebnis hängt nur von den go.mod-Dateien im Graph ab, nicht von der Uhrzeit. Derselbe Code baut heute und in drei Jahren mit denselben Versionen.

Der Trade-off: Updates passieren nie automatisch. Wenn eine Abhängigkeit einen Sicherheitsfix veröffentlicht, bekommst du ihn erst, wenn du go get ausführst. Deshalb gehört govulncheck zum Arbeitsablauf und nicht in die Kategorie „nice to have“.

Die vollständige Liste aller Module, die MVS ausgewählt hat, zeigt go list -m all. Sie ist länger als go.mod, weil sie auch Module enthält, die zwar im Abhängigkeitsgraph vorkommen, aber für den Build der upwatch-Pakete nicht gebraucht werden (etwa golang.org/x/tools, das nur die Tests von modernc.org/libc brauchen – go mod why -m golang.org/x/tools zeigt die Kette).

Modul-Proxy und Checksum-Datenbank

Woher kommen die Module? Standardmäßig nicht direkt von GitHub:

go env GOPROXY GOSUMDB
https://proxy.golang.org,direct
sum.golang.org

proxy.golang.org ist ein Cache, den das Go-Team betreibt. Er hält jede einmal angefragte Version dauerhaft vor. Wird ein Repository gelöscht oder ein Tag umgeschrieben, baut dein Projekt trotzdem weiter – ein Unterschied zu Ökosystemen, in denen das Entfernen eines Pakets fremde Builds brechen kann. direct heißt: Wenn der Proxy ein Modul nicht kennt, wird es direkt aus dem Versionskontrollsystem geholt.

sum.golang.org ist eine öffentliche, nur anhängbare Datenbank mit den Prüfsummen aller öffentlichen Modulversionen. Wenn du ein Modul zum ersten Mal hinzufügst und noch kein go.sum-Eintrag existiert, gleicht die Toolchain den Hash damit ab. Damit kann auch ein kompromittierter Proxy dir keine manipulierte Version unterschieben, ohne dass es auffällt.

Für private Module in Firmen-Repositorys setzt du GOPRIVATE (zum Beispiel GOPRIVATE=github.com/meinefirma/*). Diese Module werden dann direkt geholt und nicht gegen die öffentliche Datenbank geprüft.

Die toolchain-Direktive

Seit Go 1.21 kann die Toolchain sich selbst aktualisieren. Verlangt die go-Zeile eines Moduls eine neuere Version als die installierte, lädt go die passende Version herunter und führt den Befehl damit aus – vorausgesetzt, GOTOOLCHAIN steht auf auto, was der Standard ist.

Mit der Direktive toolchain legst du zusätzlich fest, welche Version bevorzugt werden soll, ohne die Mindestanforderung zu erhöhen:

go mod edit -toolchain=go1.27.2
module example.com/hello

go 1.26.0

toolchain go1.27.2

Das heißt: Das Modul funktioniert ab Go 1.26.0, aber wer im Projekt arbeitet, nutzt 1.27.2. Ist lokal nur 1.27.1 installiert, meldet der nächste go-Befehl:

go: downloading go1.27.2 (darwin/arm64)

Die heruntergeladene Toolchain landet im Modul-Cache; deine Installation bleibt unverändert. Eine vergleichbare Funktion bietet in .NET die global.json, in Node das Feld engines zusammen mit einem Versionsmanager – nur dass Go das selbst erledigt. Praktisch wird das, wenn govulncheck Schwachstellen in der Standardbibliothek meldet: Die steckt im Binary, also hilft dort kein go get, sondern nur eine neuere Go-Version (Teil 13).

Build-Optionen

Das Makefile baut upwatch mit ein paar zusätzlichen Optionen:

CGO_ENABLED=0 go build -trimpath -ldflags="-s -w -X main.version=v1.0.0" -o bin/upwatch ./cmd/upwatch

CGO_ENABLED=0 erzwingt ein Binary ohne C-Anbindung, -trimpath entfernt lokale Pfade, -ldflags macht die Datei kleiner und schreibt die Version in die Variable version in main.go. Was die Optionen genau bewirken und wie viel sie bringen, steht in Teil 15. Welche Go-Version und welche Modulversionen in einem fertigen Binary stecken, liest go version -m bin/upwatch aus – auch Jahre später.

Im Projekt

  • go.mod und go.sum – eine direkte Abhängigkeit, der Rest indirekt über den SQLite-Treiber.
  • Makefile – make check führt aus, was auch die CI prüft: gofmt -l, go vet, go test -race und die Beispiele.
  • .github/workflows/ci.yml – der Hauptjob nimmt die aktuelle stabile Go-Version (sonst würde govulncheck die Schwachstellen eines alten Patch-Releases melden) und führt gofmt, vet, Tests mit Race-Detector, govulncheck und Cross-Compilation aus. Ein zweiter Job liest die Mindestversion aus go.mod (go-version-file) und prüft, dass upwatch damit noch baut und die Tests besteht.
  • Dockerfile – kopiert zuerst nur go.mod und go.sum und führt go mod download aus, damit diese Schicht im Docker-Cache bleibt, solange sich die Abhängigkeiten nicht ändern.

Übungen

Musterlösungen mit Begründung stehen im Branch loesungen – versuch es zuerst selbst.

  1. Aufwärmen: Rücke in deinem vetdemo-Modul eine Zeile mit Leerzeichen statt Tab ein und füge überflüssige Leerzeichen um ein Komma ein. Was meldet gofmt -l ., was ändert go fmt ./...?
  2. Kern: Lege außerhalb des Repositorys ein Modul example.com/ping an, schreibe ein Programm, das https://example.com mit http.Get abruft und den Statuscode ausgibt, und baue es mit GOOS=linux GOARCH=arm64 go build. Prüfe das Ergebnis mit file und go version -m.
  3. Kern: Führe im upwatch-Repository go mod why -m golang.org/x/sys und go list -m all | wc -l aus. Warum stehen in go list -m all mehr Module als in go.mod?
  4. Profi: Setze im ping-Modul aus Übung 2 mit go mod edit -toolchain=… eine neuere Patch-Version, als du installiert hast, und führe danach go version aus. Was lädt Go herunter, wo landet es, und was ändert sich an deiner Go-Installation? Was passiert, wenn du GOTOOLCHAIN=local go version aufrufst?

Diese Seite auf GitHub bearbeiten