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 checkführt aus, was auch die CI prüft:gofmt -l,go vet,go test -raceund 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.modundgo.sumund führtgo mod downloadaus, 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.
- 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 meldetgofmt -l ., was ändertgo fmt ./...? - Kern: Lege außerhalb des Repositorys ein Modul
example.com/pingan, schreibe ein Programm, dashttps://example.commithttp.Getabruft und den Statuscode ausgibt, und baue es mitGOOS=linux GOARCH=arm64 go build. Prüfe das Ergebnis mitfileundgo version -m. - Kern: Führe im upwatch-Repository
go mod why -m golang.org/x/sysundgo list -m all | wc -laus. Warum stehen ingo list -m allmehr Module als ingo.mod? - Profi: Setze im
ping-Modul aus Übung 2 mitgo mod edit -toolchain=…eine neuere Patch-Version, als du installiert hast, und führe danachgo versionaus. Was lädt Go herunter, wo landet es, und was ändert sich an deiner Go-Installation? Was passiert, wenn duGOTOOLCHAIN=local go versionaufrufst?