Zurück zum Blog

Alle Modernizer in `go fix` 1.26 im Überblick

Go 1.26 hat go fix auf dem Analyzer-Framework neu aufgebaut. Hier findest du alle mitgelieferten Modernizer: 26 Vorher-Nachher-Diffs quer durch Sprache und Standardbibliothek.

Alle Modernizer in `go fix` 1.26 im Überblick

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.bashbash
go 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

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.gogo
func log(args ...interface{}) {
    fmt.Println(args...)
}

var cache map[string]interface{}

Nachher:

example.gogo
func 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.gogo
x := f()
if x < 0 {
    x = 0
}
if x > 100 {
    x = 100
}

Nachher:

example.gogo
x := 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.gogo
for i := 0; i < n; i++ {
    fmt.Println(i)
}

Nachher:

example.gogo
for 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.gogo
for _, x := range items {
    x := x // pre-1.22 capture trick
    go func() { use(x) }()
}

Nachher:

example.gogo
for _, 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.gogo
func strPtr(s string) *string {
    p := s
    return &p
}

cfg.Name = strPtr("LevelUpGo")

Nachher:

example.gogo
cfg.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.gogo
sort.Slice(s, func(i, j int) bool {
    return s[i] < s[j]
})

Nachher:

example.gogo
slices.Sort(s)

slicescontains (Go 1.21): Aus manuellen Suchschleifen wird slices.Contains

Vorher:

example.gogo
func has(s []int, x int) bool {
    for _, v := range s {
        if v == x {
            return true
        }
    }
    return false
}

Nachher:

example.gogo
func 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.gogo
for i := len(s) - 1; i >= 0; i-- {
    use(i, s[i])
}

Nachher:

example.gogo
for 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.gogo
func 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.gogo
func 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.gogo
keys := make([]string, 0, len(m))
for k := range m {
    keys = append(keys, k)
}
for _, k := range keys {
    use(k)
}

Nachher:

example.gogo
for 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.gogo
t := reflect.TypeOf((*MyType)(nil)).Elem()

Nachher:

example.gogo
t := 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.gogo
type User struct {
    Name    string    `json:"name,omitempty"`
    Created time.Time `json:"created,omitempty"`
}

Nachher:

example.gogo
type 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.gogo
b := []byte(fmt.Sprintf("%s=%d", key, val))

Nachher:

example.gogo
b := 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.gogo
i := strings.Index(s, "=")
if i >= 0 {
    key, val := s[:i], s[i+1:]
    use(key, val)
}

Nachher:

example.gogo
if 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.gogo
if strings.HasPrefix(s, "go-") {
    rest := strings.TrimPrefix(s, "go-")
    handle(rest)
}

Nachher:

example.gogo
if 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.gogo
for _, line := range strings.Split(s, "\n") {
    handle(line)
}

Nachher:

example.gogo
for 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.gogo
s := ""
for _, p := range parts {
    s += p
}
return s

Nachher:

example.gogo
var 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.gogo
addr := fmt.Sprintf("%s:%d", host, port)
net.Dial("tcp", addr)

Nachher:

example.gogo
addr := 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.gogo
var myerr *MyErr
if errors.As(err, &myerr) {
    handle(myerr)
}

Nachher:

example.gogo
if 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.gogo
p := unsafe.Pointer(uintptr(base) + uintptr(off))

Nachher:

example.gogo
p := 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.gogo
var wg sync.WaitGroup
wg.Add(1)
go func() {
    defer wg.Done()
    fmt.Println("go!")
}()
wg.Wait()

Nachher:

example.gogo
var 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.gogo
var count int64
atomic.AddInt64(&count, 1)
n := atomic.LoadInt64(&count)

Nachher:

example.gogo
var 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.gogo
func TestThing(t *testing.T) {
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()
    work(ctx)
}

Nachher:

example.gogo
func 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.gogo
func BenchmarkThing(b *testing.B) {
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        work()
    }
}

Nachher:

example.gogo
func 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:

  1. Starte auf einem sauberen Branch ohne andere laufende Änderungen.
  2. Führe zuerst go fix -diff ./... aus. Das Flag gibt einen Unified Diff auf stdout aus, statt Dateien zu schreiben. Überfliege ihn.
  3. Führe go fix ./... aus, um die Änderungen anzuwenden.
  4. 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.
  5. Lass deine Tests, deinen Linter und go build ./... laufen.
  6. 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.gogo
x := f()
if x < 0 {
    x = 0
}
if x > 100 {
    x = 100
}

Der erste Durchlauf schreibt die untere Grenze in max um:

example.gogo
x := max(0, f())
if x > 100 {
    x = 100
}

Der zweite Durchlauf erkennt das verbliebene Muster aus if und Zuweisung und erledigt den Rest:

example.gogo
x := 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, wenn go.mod die passende Mindestversion deklariert. Wenn eine erwartete Umschreibung ausbleibt, prüfe deine go-Direktive.
  • Für Migrationen, die nur deine Codebase betreffen, nutze die Direktive //go:fix inline. Markiere einen Wrapper oder eine Konstante, führe go fix aus, 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.

Mehr zum Thema

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.

Schreib Go wie ein Senior Engineer

Interaktive Lektionen im Browser. Die ersten sind kostenlos.

Kostenlose Lektion testenOder kostenloses Konto erstellen