Go 1.26 hat go fix auf demselben Framework wie go vet neu geschrieben. Der neue Befehl bündelt alle Modernizer, die das Go-Team und die Autoren von x/tools gebaut haben. Jeder davon ist ein kleiner Analyzer, der ein altes Muster erkennt und es in aktuelles Go umschreibt.
Ein einziger Befehl führt sie alle aus:
example.bashbashgo fix ./...
Führe ihn auf einem sauberen Branch aus, prüfe den Diff, lass deine Tests laufen und committe. Wenn du dir die Änderungen ansehen willst, ohne Dateien zu schreiben, nutze go fix -diff ./....
Unten findest du jeden Modernizer, der mit 1.26 ausgeliefert wird, nach Bereichen gruppiert. Jeder Eintrag nennt die Go-Version, die die API eingeführt hat, auf die umgeschrieben wird, dazu ein Vorher-Nachher-Paar. Die Liste ist genauso zum Überfliegen oder Durchsuchen mit Strg+F gedacht wie zum Lesen.
Inhaltsverzeichnis
- Sprache und Builtins
- Slices, Maps und Iteration
- Strings und Formatierung
- Netzwerk, Fehler und Reflection
- Nebenläufigkeit und Tests
- go fix sicher ausführen
Sprache und Builtins
Hier gibt es sieben Modernizer. Die meisten decken Sprachfeatures ab (any, die neue Form range n, die Änderung an Schleifenvariablen in 1.22, die Erweiterung new(expr)) und dazu ein paar versionsübergreifende Aufräumarbeiten.
any (Go 1.18): interface{} durch any ersetzen
any ist nur ein Alias, liest sich in Signaturen und Typparametern aber besser.
Vorher:
example.gogofunc log(args ...interface{}) { fmt.Println(args...) } var cache map[string]interface{}
Nachher:
example.gogofunc log(args ...any) { fmt.Println(args...) } var cache map[string]any
minmax (Go 1.21): if/else-Begrenzungen durch min und max ersetzen
Die Builtins min und max sind variadisch und funktionieren mit jedem geordneten Typ.
Vorher:
example.gogox := f() if x < 0 { x = 0 } if x > 100 { x = 100 }
Nachher:
example.gogox := max(0, min(100, f()))
rangeint (Go 1.22): for-Zählschleifen als for i := range n
Eine einfache Zählschleife, hinter der kein Slice und kein Array steht, kann direkt über die Zahl iterieren.
Vorher:
example.gogofor i := 0; i < n; i++ { fmt.Println(i) }
Nachher:
example.gogofor i := range n { fmt.Println(i) }
forvar (Go 1.22): den Capture-Trick x := x entfernen
Vor 1.22 teilten sich alle Iterationen dieselbe Schleifenvariable. Closures und Goroutines, die sie eingefangen haben, brauchten deshalb x := x im Schleifenrumpf. Seit 1.22 bekommt jede Iteration ihre eigene Variable, womit das Shadowing überflüssig wird.
Vorher:
example.gogofor _, x := range items { x := x // pre-1.22 capture trick go func() { use(x) }() }
Nachher:
example.gogofor _, x := range items { go func() { use(x) }() }
Der Analyzer greift nur, wenn go.mod go 1.22 oder neuer deklariert, da für ältere Module noch die Semantik der geteilten Variable gilt.
newexpr (Go 1.26): Pointer auf einen Wert mit new(expr)
1.26 erweitert das Builtin new, sodass es einen Ausdruck akzeptiert und nicht nur einen Typ. new(s) liefert einen *string, der auf eine Kopie von s zeigt. Damit werden die üblichen Hilfsfunktionen wie strPtr/intPtr/boolPtr überflüssig.
Vorher:
example.gogofunc strPtr(s string) *string { p := s return &p } cfg.Name = strPtr("LevelUpGo")
Nachher:
example.gogocfg.Name = new("LevelUpGo")
Wenn du Hilfsfunktionen hast, die du noch nicht löschen kannst (weitere Aufrufer, exportierte API), findest du unten bei inline die Direktive, mit der du schrittweise migrieren kannst.
inline (Direktive): mit //go:fix inline markierte Funktionen und Konstanten inlinen
Füge die Direktive zu einem Wrapper hinzu, und go fix schreibt jede Aufrufstelle so um, dass sie den inneren Ausdruck verwendet. Ich finde das praktisch für Umbenennungen und um dünne Wrapper loszuwerden. Du kannst damit auch eigene Modernizer in einer privaten Codebase ausrollen.
Vorher:
example.gogo//go:fix inline func Square(x int) int { return Pow(x, 2) } a := Square(5) b := Square(10)
Nachher:
example.gogo//go:fix inline func Square(x int) int { return Pow(x, 2) } a := Pow(5, 2) b := Pow(10, 2)
Dieselbe Direktive funktioniert auch bei Konstanten. Dadurch eignet sie sich für die Migration zwischen umbenannten Enum-Werten.
plusbuild (Go 1.17): // +build durch //go:build ersetzen
Die alte Build-Tag-Syntax ist seit 1.17 veraltet. Die neue Direktive verwendet normale boolesche Ausdrücke in Go-Syntax.
Vorher:
example.gogo// +build linux,amd64 package sys
Nachher:
example.gogo//go:build linux && amd64 package sys
Slices, Maps und Iteration
Fünf der sieben Modernizer in dieser Gruppe stellen handgeschriebene Schleifen auf die Pakete slices und maps um. Die anderen beiden kümmern sich um reflect.TypeFor und um omitzero in JSON-Tags.
slicessort (Go 1.21): Aus sort.Slice wird slices.Sort
Für den häufigen Fall, einen Slice mit geordnetem Typ aufsteigend zu sortieren, ist slices.Sort generisch und kommt ganz ohne Vergleichs-Closure aus.
Vorher:
example.gogosort.Slice(s, func(i, j int) bool { return s[i] < s[j] })
Nachher:
example.gogoslices.Sort(s)
slicescontains (Go 1.21): Aus manuellen Suchschleifen wird slices.Contains
Vorher:
example.gogofunc has(s []int, x int) bool { for _, v := range s { if v == x { return true } } return false }
Nachher:
example.gogofunc has(s []int, x int) bool { return slices.Contains(s, x) }
slicesbackward (Go 1.23): rückwärts iterieren mit slices.Backward
slices.Backward liefert einen Iterator, der Paare aus Index und Wert vom Ende des Slice zum Anfang hin ausgibt.
Vorher:
example.gogofor i := len(s) - 1; i >= 0; i-- { use(i, s[i]) }
Nachher:
example.gogofor i, v := range slices.Backward(s) { use(i, v) }
mapsloop (Go 1.21): Aus Copy-, Clone- und Insert-Schleifen werden maps-Aufrufe
Das Paket maps hat für jede der üblichen Schleifen über Maps eine eigene Funktion. So verrät schon der Name des Aufrufs, was die Schleife gemacht hat.
Vorher:
example.gogofunc copyMap(src map[string]int) map[string]int { dst := make(map[string]int, len(src)) for k, v := range src { dst[k] = v } return dst }
Nachher:
example.gogofunc copyMap(src map[string]int) map[string]int { dst := make(map[string]int, len(src)) maps.Copy(dst, src) return dst }
Derselbe Modernizer deckt auch maps.Clone, maps.Equal und das Muster ab, eine Map aus Paaren zu befüllen.
stditerators (Go 1.23): Iterator-Funktionen der Standardbibliothek in range-Schleifen nutzen
maps.Keys, maps.Values, slices.All, slices.Values und Co. liefern Iteratoren, über die du direkt mit range iterieren kannst, ohne vorher einen Zwischen-Slice zu bauen.
Vorher:
example.gogokeys := make([]string, 0, len(m)) for k := range m { keys = append(keys, k) } for _, k := range keys { use(k) }
Nachher:
example.gogofor k := range maps.Keys(m) { use(k) }
reflecttypefor (Go 1.22): den nil-Pointer-Trick für reflect entfernen
reflect.TypeFor[T]() ist der typisierte, generische Weg, einen reflect.Type für einen statischen Typ zu bekommen. Der alte Workaround TypeOf((*T)(nil)).Elem() stammt aus der Zeit vor Generics.
Vorher:
example.gogot := reflect.TypeOf((*MyType)(nil)).Elem()
Nachher:
example.gogot := reflect.TypeFor[MyType]()
omitzero (Go 1.24): fehleranfälliges omitempty durch omitzero ersetzen
omitempty richtet sich danach, ob ein Wert in Go als leer gilt. Ein time.Time{} ist in diesem Sinn nicht „leer“ (es ist ein Struct mit Feldern) und wird deshalb nie weggelassen. omitzero ruft die Methode IsZero des Werts auf, falls vorhanden. So verschwinden Nullwerte von time.Time wie erwartet aus der Ausgabe.
Der Analyzer ändert nur Felder, bei denen omitzero sicher ist. Strings, Slices und Pointer bleiben bei omitempty, weil sich sonst ihr Verhalten ändern würde.
Vorher:
example.gogotype User struct { Name string `json:"name,omitempty"` Created time.Time `json:"created,omitempty"` }
Nachher:
example.gogotype User struct { Name string `json:"name,omitempty"` Created time.Time `json:"created,omitzero"` }
Strings und Formatierung
Diese fünf ersetzen ältere Muster mit strings und fmt durch neuere APIs. Manche sparen eine Allokation, andere lesen sich einfach leichter.
fmtappendf (Go 1.19): Aus []byte(fmt.Sprintf(...)) wird fmt.Appendf
fmt.Appendf schreibt formatierte Ausgabe direkt in ein []byte, das du übergibst (oder nil für eine frische Allokation), und überspringt dabei den Zwischen-String.
Vorher:
example.gogob := []byte(fmt.Sprintf("%s=%d", key, val))
Nachher:
example.gogob := fmt.Appendf(nil, "%s=%d", key, val)
stringscut (Go 1.18): Aus strings.Index plus Slicing wird strings.Cut
strings.Cut liefert Präfix, Suffix und einen bool für „gefunden“ in einem einzigen Aufruf. Außerdem behandelt es den Fall, dass nichts gefunden wird, explizit.
Vorher:
example.gogoi := strings.Index(s, "=") if i >= 0 { key, val := s[:i], s[i+1:] use(key, val) }
Nachher:
example.gogoif key, val, ok := strings.Cut(s, "="); ok { use(key, val) }
stringscutprefix (Go 1.20): Aus HasPrefix plus TrimPrefix wird CutPrefix
CutPrefix liefert den gekürzten String und einen bool für „gefunden“, sodass du die Präfixprüfung nur einmal machst.
Vorher:
example.gogoif strings.HasPrefix(s, "go-") { rest := strings.TrimPrefix(s, "go-") handle(rest) }
Nachher:
example.gogoif rest, ok := strings.CutPrefix(s, "go-"); ok { handle(rest) }
Derselbe Modernizer kümmert sich auch um CutSuffix.
stringsseq (Go 1.24): Aus Split und Fields in range-Schleifen werden SplitSeq und FieldsSeq
SplitSeq und FieldsSeq liefern Iteratoren, statt einen Slice zu allozieren. Wenn du die Teile nur einmal durchlaufen musst, spart die Iterator-Variante die Allokation komplett.
Vorher:
example.gogofor _, line := range strings.Split(s, "\n") { handle(line) }
Nachher:
example.gogofor line := range strings.SplitSeq(s, "\n") { handle(line) }
Der Analyzer schreibt nur Fälle um, in denen der Slice einmal durchlaufen und dann verworfen wird. Speicherst du den Slice oder greifst per Index darauf zu, bleibt die ursprüngliche Split-Form stehen.
stringsbuilder (Go 1.10): Aus quadratischer Verkettung in Schleifen wird strings.Builder
s += p in einer Schleife alloziert und kopiert bei jeder Iteration neu, was auf O(n²) Aufwand hinausläuft. strings.Builder wächst wie ein Slice.
Vorher:
example.gogos := "" for _, p := range parts { s += p } return s
Nachher:
example.gogovar sb strings.Builder for _, p := range parts { sb.WriteString(p) } return sb.String()
Der Analyzer ist hier zurückhaltend. Er greift nur beim offensichtlichen Akkumulator-Muster, nicht bei jedem += in einer Schleife. Wenn deine Schleife zwei oder drei kleine Konstanten verkettet, ist die einfachere Form in Ordnung.
Netzwerk, Fehler und Reflection
Diese Gruppe umfasst drei: eine alte Stolperfalle in net, das neue generische errors.AsType und die sichereren unsafe-Helfer.
hostport (Go 1.0): Aus Sprintf für Adressen wird net.JoinHostPort
fmt.Sprintf("%s:%d", host, port) baut eine kaputte Adresse, wenn host ein IPv6-Literal ist, denn IPv6-Adressen enthalten selbst Doppelpunkte. net.JoinHostPort setzt IPv6-Hosts in eckige Klammern und ist der kanonische Weg, einen host:port-String zu bauen.
Vorher:
example.gogoaddr := fmt.Sprintf("%s:%d", host, port) net.Dial("tcp", addr)
Nachher:
example.gogoaddr := net.JoinHostPort(host, strconv.Itoa(port)) net.Dial("tcp", addr)
errorsastype (Go 1.26): Aus errors.As wird das generische errors.AsType
1.26 führt errors.AsType[T] ein. Es liefert den typisierten Fehler und einen bool, statt dass du eine Zielvariable deklarieren und ihre Adresse übergeben musst.
Vorher:
example.gogovar myerr *MyErr if errors.As(err, &myerr) { handle(myerr) }
Nachher:
example.gogoif myerr, ok := errors.AsType[*MyErr](err); ok { handle(myerr) }
unsafefuncs (Go 1.17): Aus unsafe-Arithmetik werden unsafe.Add und unsafe.Slice
1.17 hat unsafe.Add und unsafe.Slice eingeführt. Damit kannst du Pointer-Arithmetik betreiben und Slices über beliebigen Speicher bauen, ohne den Umweg über uintptr von Hand zu schreiben.
Vorher (base ist ein unsafe.Pointer):
example.gogop := unsafe.Pointer(uintptr(base) + uintptr(off))
Nachher:
example.gogop := unsafe.Add(base, off)
Derselbe Modernizer schreibt manuelle Muster aus unsafe.Pointer plus reflect.SliceHeader in unsafe.Slice um.
Nebenläufigkeit und Tests
Die letzten vier räumen Code auf, der sync, atomic und das Paket testing nutzt.
waitgroupgo (Go 1.25): Aus Add(1) plus defer Done() wird wg.Go
1.25 hat WaitGroup.Go eingeführt. Es fasst das Add, den Start der Goroutine und das defer Done in einem Aufruf zusammen.
Vorher:
example.gogovar wg sync.WaitGroup wg.Add(1) go func() { defer wg.Done() fmt.Println("go!") }() wg.Wait()
Nachher:
example.gogovar wg sync.WaitGroup wg.Go(func() { fmt.Println("go!") }) wg.Wait()
atomictypes (Go 1.19): Aus Atomic-Funktionen werden Methoden
atomic.Int64, atomic.Uint64, atomic.Pointer[T] und Co. tragen ihren Wert selbst. Du reichst also keine Pointer mehr herum, und die Größe der Operation kann nicht mehr vom Typ der Variable abweichen.
Vorher:
example.gogovar count int64 atomic.AddInt64(&count, 1) n := atomic.LoadInt64(&count)
Nachher:
example.gogovar count atomic.Int64 count.Add(1) n := count.Load()
testingcontext (Go 1.24): Aus manuellem WithCancel in Tests wird t.Context()
t.Context() liefert einen Context, der automatisch abgebrochen wird, wenn der Test endet. Du musst nicht an defer cancel() denken, und der Context wird an Subtests weitergereicht.
Vorher:
example.gogofunc TestThing(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() work(ctx) }
Nachher:
example.gogofunc TestThing(t *testing.T) { ctx := t.Context() work(ctx) }
bloop (Go 1.24): Aus for i := 0; i < b.N; i++ wird for b.Loop()
b.Loop() ist die neue Benchmark-Schleife. Sie kümmert sich außerdem automatisch um das Zurücksetzen und Anhalten des Timers. In den meisten Benchmarks fallen die manuellen Aufrufe von b.ResetTimer() und b.StopTimer() rund um das Setup deshalb weg.
Vorher:
example.gogofunc BenchmarkThing(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { work() } }
Nachher:
example.gogofunc BenchmarkThing(b *testing.B) { for b.Loop() { work() } }
go fix sicher ausführen
go fix schreibt echte Quelldateien um. Behandle es wie jedes andere automatisierte Refactoring.
Eine sichere Reihenfolge:
- Starte auf einem sauberen Branch ohne andere laufende Änderungen.
- Führe zuerst
go fix -diff ./...aus. Das Flag gibt einen Unified Diff auf stdout aus, statt Dateien zu schreiben. Überfliege ihn. - Führe
go fix ./...aus, um die Änderungen anzuwenden. - Führe
git diffüber die tatsächlichen Änderungen aus. Achte auf jede Umschreibung, die dich überrascht, besonders in Code, der absichtlich in einem älteren Stil geschrieben wurde. - Lass deine Tests, deinen Linter und
go build ./...laufen. - Committe.
Ein Durchlauf reicht oft nicht
Ein Fix kann die Gelegenheit für einen weiteren schaffen, und die Analyzer verfolgen diese Kaskade nicht innerhalb eines einzigen Durchlaufs.
Die Begrenzung aus dem Abschnitt zu minmax zeigt das. Ausgangspunkt ist:
example.gogox := f() if x < 0 { x = 0 } if x > 100 { x = 100 }
Der erste Durchlauf schreibt die untere Grenze in max um:
example.gogox := max(0, f()) if x > 100 { x = 100 }
Der zweite Durchlauf erkennt das verbliebene Muster aus if und Zuweisung und erledigt den Rest:
example.gogox := min(100, max(0, f()))
Es braucht zwei Durchläufe, weil jeder Modernizer ein eigener Analyzer ist, der sich deinen Code pro go fix-Lauf einmal ansieht. Er durchsucht den Syntaxbaum, findet die Muster, die er kennt, und erzeugt Fixes dafür.
Im Begrenzungsbeispiel schreibt der erste Durchlauf den Block mit < 0 um. Nach dieser Änderung folgt der Block mit > 100 auf einen Aufruf von max(...) statt auf ein nacktes x := f(), und das ist eine neue Gelegenheit für minmax. minmax hat seinen einen Durchlauf aber schon hinter sich. Die neue Form sieht er deshalb erst, wenn du go fix noch einmal ausführst.
Dasselbe passiert auch zwischen verschiedenen Analyzern. Eine Umschreibung durch mapsloop kann eine Gelegenheit für stditerators schaffen, weil der Iterator-Analyzer nach einer Form wie range maps.Keys(m) sucht, die es vor der Umschreibung der Schleife nicht gab.
Die Lösung ist, go fix ./... so lange erneut auszuführen, bis der Diff leer zurückkommt. Zwei Durchläufe reichen für die meisten Codebases, und ein dritter ist selten.
Weitere Hinweise
- Die versionsabhängigen Modernizer (
forvar,rangeint,newexpr,errorsastype) greifen nur, wenngo.moddie passende Mindestversion deklariert. Wenn eine erwartete Umschreibung ausbleibt, prüfe deinego-Direktive. - Für Migrationen, die nur deine Codebase betreffen, nutze die Direktive
//go:fix inline. Markiere einen Wrapper oder eine Konstante, führego fixaus, und jede Aufrufstelle wird direkt an Ort und Stelle aktualisiert. - Wenn eine Umschreibung das Verhalten auf eine Weise ändert, die der Analyzer nicht erkannt hat, ist das ein Bug im Analyzer, und es lohnt sich, ein Issue dafür anzulegen. Das Go-Team kümmert sich schnell um solche Meldungen.
Den vollständigen Überblick über das Release Go 1.26 (neue Krypto-APIs, Green Tea GC, Erkennung von Goroutine-Leaks und alles Weitere) findest du im Leitfaden zu den Neuerungen in Go 1.26.
Direkt beim Go-Projekt beschreibt die Doku zu x/tools/go/analysis/passes/modernize jeden Analyzer im Detail, und der Beitrag im Go-Blog zu go fix erklärt das neue Framework.
