Go 1.26 rewrote go fix on the same framework as go vet. The new command bundles every modernizer the Go team and the x/tools authors have built. Each one is a small analyzer that recognizes an old pattern and rewrites it into current Go.
One command runs them all:
example.bashbashgo fix ./...
Run it on a clean branch, review the diff, run your tests, commit. If you want to preview without writing files, use go fix -diff ./....
This post is the catalog. Every modernizer that ships in 1.26, grouped by area, with the version that introduced the target API and a before/after pair for each one. Skim, ctrl-F, or read top to bottom.
Table of Contents
- Language and Builtins
- Slices, Maps, and Iteration
- Strings and Formatting
- Networking, Errors, and Reflection
- Concurrency and Testing
- Running go fix safely
Language and Builtins
Seven modernizers in this group. They cover language features (any, the new range n form, the loop-variable change in 1.22, the new(expr) extension) plus a couple of cross-version cleanups.
any (Go 1.18): replace interface{} with any
any is just an alias, but it reads better in signatures and type parameters.
Before:
example.gogofunc log(args ...interface{}) { fmt.Println(args...) } var cache map[string]interface{}
After:
example.gogofunc log(args ...any) { fmt.Println(args...) } var cache map[string]any
minmax (Go 1.21): replace if/else clamps with min and max
The min and max builtins are variadic and work on any ordered type.
Before:
example.gogox := f() if x < 0 { x = 0 } if x > 100 { x = 100 }
After:
example.gogox := max(0, min(100, f()))
rangeint (Go 1.22): integer for loops as for i := range n
Counting loops without an array or slice get a cleaner form.
Before:
example.gogofor i := 0; i < n; i++ { fmt.Println(i) }
After:
example.gogofor i := range n { fmt.Println(i) }
forvar (Go 1.22): drop the x := x capture trick
Before 1.22, the loop variable was shared across iterations, so closures and goroutines that captured it needed x := x inside the loop body. 1.22 made each iteration get its own variable, which makes the shadow redundant.
Before:
example.gogofor _, x := range items { x := x // pre-1.22 capture trick go func() { use(x) }() }
After:
example.gogofor _, x := range items { go func() { use(x) }() }
The analyzer only triggers when go.mod declares go 1.22 or later, since older modules still have the shared-variable semantics.
newexpr (Go 1.26): pointer-to-value with new(expr)
1.26 extends the new builtin so it accepts an expression, not just a type. new(s) returns a *string pointing to a copy of s. That kills a category of strPtr/intPtr/boolPtr helpers.
Before:
example.gogofunc strPtr(s string) *string { p := s return &p } cfg.Name = strPtr("LevelUpGo")
After:
example.gogocfg.Name = new("LevelUpGo")
If you have helpers you can't delete yet (other callers, exported API), see inline below for the directive that lets you migrate gradually.
inline (directive): inline functions and constants tagged with //go:fix inline
Add the directive to a wrapper, and go fix rewrites every call site to use the inner expression. Handy for renaming, deprecating thin wrappers, or rolling your own modernizers across a private codebase.
Before:
example.gogo//go:fix inline func Square(x int) int { return Pow(x, 2) } a := Square(5) b := Square(10)
After:
example.gogo//go:fix inline func Square(x int) int { return Pow(x, 2) } a := Pow(5, 2) b := Pow(10, 2)
The same directive works on constants, which makes it useful for migrating between renamed enum values.
plusbuild (Go 1.17): replace // +build with //go:build
The legacy build-tag syntax has been deprecated since 1.17. The new directive uses normal Go boolean expressions.
Before:
example.gogo// +build linux,amd64 package sys
After:
example.gogo//go:build linux && amd64 package sys
Slices, Maps, and Iteration
Seven modernizers that move handwritten loops over to the slices and maps packages, plus one for reflect.TypeFor and one for omitzero JSON tags.
slicessort (Go 1.21): sort.Slice becomes slices.Sort
For the common case of sorting an ordered slice ascending, slices.Sort is generic and avoids the comparator closure entirely.
Before:
example.gogosort.Slice(s, func(i, j int) bool { return s[i] < s[j] })
After:
example.gogoslices.Sort(s)
slicescontains (Go 1.21): manual lookup loops become slices.Contains
Before:
example.gogofunc has(s []int, x int) bool { for _, v := range s { if v == x { return true } } return false }
After:
example.gogofunc has(s []int, x int) bool { return slices.Contains(s, x) }
slicesbackward (Go 1.23): reverse iteration with slices.Backward
slices.Backward returns an iterator that yields index/value pairs from the end of the slice to the start.
Before:
example.gogofor i := len(s) - 1; i >= 0; i-- { use(i, s[i]) }
After:
example.gogofor i, v := range slices.Backward(s) { use(i, v) }
mapsloop (Go 1.21): copy/clone/insert loops become maps calls
The maps package replaces the most common map-loop patterns with one-liners that document intent.
Before:
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 }
After:
example.gogofunc copyMap(src map[string]int) map[string]int { dst := make(map[string]int, len(src)) maps.Copy(dst, src) return dst }
The same modernizer covers maps.Clone, maps.Equal, and the insert-from-pairs pattern.
stditerators (Go 1.23): use stdlib iterator functions in range loops
maps.Keys, maps.Values, slices.All, slices.Values, and friends return iterators you can range over directly. No more building an intermediate slice.
Before:
example.gogokeys := make([]string, 0, len(m)) for k := range m { keys = append(keys, k) } for _, k := range keys { use(k) }
After:
example.gogofor k := range maps.Keys(m) { use(k) }
reflecttypefor (Go 1.22): drop the nil-pointer reflect trick
reflect.TypeFor[T]() is the typed, generic way to get a reflect.Type for a static type. The old TypeOf((*T)(nil)).Elem() workaround predates generics.
Before:
example.gogot := reflect.TypeOf((*MyType)(nil)).Elem()
After:
example.gogot := reflect.TypeFor[MyType]()
omitzero (Go 1.24): swap fragile omitempty for omitzero
omitempty looks at the Go zero-ness of a value, which means a time.Time{} is not "empty" (it's a struct with fields), so it never gets omitted. omitzero calls the value's IsZero method when present, so time.Time zero values disappear from the output as you'd expect.
The analyzer only changes fields where omitzero is safe. Strings, slices, and pointers stay on omitempty because their behavior would change.
Before:
example.gogotype User struct { Name string `json:"name,omitempty"` Created time.Time `json:"created,omitempty"` }
After:
example.gogotype User struct { Name string `json:"name,omitempty"` Created time.Time `json:"created,omitzero"` }
Strings and Formatting
Five modernizers, all about replacing older strings and fmt patterns with newer APIs that are either faster, allocation-free, or easier to read.
fmtappendf (Go 1.19): []byte(fmt.Sprintf(...)) becomes fmt.Appendf
fmt.Appendf writes formatted output directly into a []byte you supply (or nil for a fresh allocation), skipping the intermediate string.
Before:
example.gogob := []byte(fmt.Sprintf("%s=%d", key, val))
After:
example.gogob := fmt.Appendf(nil, "%s=%d", key, val)
stringscut (Go 1.18): strings.Index plus slicing becomes strings.Cut
strings.Cut returns the prefix, the suffix, and a found bool in one call. It also handles the not-found case explicitly.
Before:
example.gogoi := strings.Index(s, "=") if i >= 0 { key, val := s[:i], s[i+1:] use(key, val) }
After:
example.gogoif key, val, ok := strings.Cut(s, "="); ok { use(key, val) }
stringscutprefix (Go 1.20): HasPrefix plus TrimPrefix becomes CutPrefix
CutPrefix returns the trimmed string and a found bool, so you only do the prefix check once.
Before:
example.gogoif strings.HasPrefix(s, "go-") { rest := strings.TrimPrefix(s, "go-") handle(rest) }
After:
example.gogoif rest, ok := strings.CutPrefix(s, "go-"); ok { handle(rest) }
The same modernizer handles CutSuffix.
stringsseq (Go 1.24): Split and Fields in range loops become SplitSeq and FieldsSeq
SplitSeq and FieldsSeq return iterators rather than allocating a slice. If you only need to walk the parts once, the iterator form skips the allocation entirely.
Before:
example.gogofor _, line := range strings.Split(s, "\n") { handle(line) }
After:
example.gogofor line := range strings.SplitSeq(s, "\n") { handle(line) }
The analyzer only rewrites cases where the slice is consumed once and discarded. If you store the slice or index into it, the original Split form stays.
stringsbuilder (Go 1.10): quadratic concatenation in loops becomes strings.Builder
s += p inside a loop reallocates and copies on every iteration, which turns into O(n²) work. strings.Builder grows like a slice.
Before:
example.gogos := "" for _, p := range parts { s += p } return s
After:
example.gogovar sb strings.Builder for _, p := range parts { sb.WriteString(p) } return sb.String()
The analyzer is conservative here. It only triggers on the obvious accumulator pattern, not every += in a loop. If your loop concatenates two or three small constants, the simpler form is fine.
Networking, Errors, and Reflection
Three modernizers covering an old net footgun, the new generic errors.AsType, and the safer unsafe helpers.
hostport (Go 1.0): Sprintf for addresses becomes net.JoinHostPort
fmt.Sprintf("%s:%d", host, port) builds a broken address when host is an IPv6 literal, since IPv6 addresses contain colons of their own. net.JoinHostPort wraps IPv6 hosts in brackets and is the canonical way to build a host:port string.
Before:
example.gogoaddr := fmt.Sprintf("%s:%d", host, port) net.Dial("tcp", addr)
After:
example.gogoaddr := net.JoinHostPort(host, strconv.Itoa(port)) net.Dial("tcp", addr)
errorsastype (Go 1.26): errors.As becomes generic errors.AsType
1.26 adds errors.AsType[T], which returns the typed error and a bool instead of asking you to declare a target variable and pass its address.
Before:
example.gogovar myerr *MyErr if errors.As(err, &myerr) { handle(myerr) }
After:
example.gogoif myerr, ok := errors.AsType[*MyErr](err); ok { handle(myerr) }
unsafefuncs (Go 1.17): unsafe arithmetic becomes unsafe.Add and unsafe.Slice
1.17 added unsafe.Add and unsafe.Slice so you can do pointer arithmetic and build slices over arbitrary memory without writing the uintptr dance by hand.
Before (base is an unsafe.Pointer):
example.gogop := unsafe.Pointer(uintptr(base) + uintptr(off))
After:
example.gogop := unsafe.Add(base, off)
The same modernizer rewrites manual unsafe.Pointer plus reflect.SliceHeader patterns into unsafe.Slice.
Concurrency and Testing
Four modernizers that clean up sync, atomic, and the testing package.
waitgroupgo (Go 1.25): Add(1) plus defer Done() becomes wg.Go
1.25 added WaitGroup.Go, which combines the Add, the goroutine launch, and the defer Done into one call.
Before:
example.gogovar wg sync.WaitGroup wg.Add(1) go func() { defer wg.Done() fmt.Println("go!") }() wg.Wait()
After:
example.gogovar wg sync.WaitGroup wg.Go(func() { fmt.Println("go!") }) wg.Wait()
atomictypes (Go 1.19): function-style atomics become method-style
The atomic.Int64, atomic.Uint64, atomic.Pointer[T], and friends carry their own value, so you stop passing pointers around and stop mismatching the operation's size with the variable's type.
Before:
example.gogovar count int64 atomic.AddInt64(&count, 1) n := atomic.LoadInt64(&count)
After:
example.gogovar count atomic.Int64 count.Add(1) n := count.Load()
testingcontext (Go 1.24): manual WithCancel in tests becomes t.Context()
t.Context() returns a context that is automatically cancelled when the test ends. No defer cancel() to remember, and the context carries through subtests.
Before:
example.gogofunc TestThing(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() work(ctx) }
After:
example.gogofunc TestThing(t *testing.T) { ctx := t.Context() work(ctx) }
bloop (Go 1.24): for i := 0; i < b.N; i++ becomes for b.Loop()
b.Loop() is the new benchmark loop. It also handles timer reset and stop automatically, which means the manual b.ResetTimer() and b.StopTimer() calls around setup go away in most benchmarks.
Before:
example.gogofunc BenchmarkThing(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { work() } }
After:
example.gogofunc BenchmarkThing(b *testing.B) { for b.Loop() { work() } }
Running go fix safely
go fix rewrites real source files. Treat it like any other automated refactor.
A workflow that has worked well on production codebases:
- Start on a clean branch with no other in-flight changes.
- Run
go fix -diff ./...first. The flag prints a unified diff to stdout instead of writing files. Skim it. - Run
go fix ./...to apply. git diffagain, this time over the actual edits. Look for any rewrite that surprises you, especially in code that was deliberately written in an older style.- Run your tests, your linter, and
go build ./.... - Commit.
One run is often not enough
Modernizers compose. Applying one fix can expose an opportunity for another, and the analyzers do not chase the cascade in a single pass.
The classic example is the minmax rewrite. Given this clamp:
example.gogox := f() if x < 0 { x = 0 } if x > 100 { x = 100 }
The first run rewrites the upper bound to max:
example.gogox := max(0, f()) if x > 100 { x = 100 }
The second run sees the remaining if/assign pattern and finishes the job:
example.gogox := min(100, max(0, f()))
Why does this need two passes? Each modernizer is its own analyzer, and each one only looks at your code once per go fix run. It scans the syntax tree, finds the patterns it knows how to rewrite, and emits the fixes.
In the clamp example, minmax looks for if x > c { x = c }. The first pass finds the > 100 block and rewrites it. That changes the file. The < 0 block now sits next to a max(...) call instead of a bare x := f(), which is a fresh minmax opportunity. But minmax already did its single pass. It won't see the new shape until you run go fix again.
The same thing happens across analyzers. A mapsloop rewrite can expose a stditerators opportunity, because the iterator analyzer was looking for a range maps.Keys(m) shape that didn't exist before the loop got rewritten.
Just re-run go fix ./... until the diff is empty. Two passes covers most codebases. Three is rare.
Other notes
- The version-gated modernizers (
forvar,rangeint,newexpr,errorsastype) only trigger ifgo.moddeclares the right minimum version. If you don't see the rewrite you expected, check yourgodirective. - The
//go:fix inlinedirective is your escape hatch for bespoke migrations. Tag a wrapper or a constant, rungo fix, and every call site updates in place. - If a rewrite changes behavior in a way the analyzer didn't catch, that's a bug in the analyzer. The Go team triages those quickly. Worth filing.
Where to learn more
For the full Go 1.26 release coverage (new crypto APIs, Green Tea GC, goroutine leak detection, the rest), see the What's New in Go 1.26 guide.
The upstream sources are good too: the x/tools/go/analysis/passes/modernize docs cover each analyzer in detail, and the Go blog post on go fix explains the new framework.
