Modernize Go code by applying version-appropriate idioms and APIs (gofix-style transformations). Scans go.mod for the Go version, then transforms Go source files to use modern patterns—from Go 1.0 through 1.26+. Use when the user says "现代化","现代Go语言", "地道的", "idiomatic", "modernize", "modern-go", "update Go code", "gofix", or wants to upgrade Go idioms.
modern-go
Modernize Go source code by applying version-appropriate idioms, APIs, and language features. Works like go fix plus additional transformations curated from the Go team's modernize analysis passes and community best practices.
Usage
Invoke this skill when the user asks to modernize Go code. By default, modernize the entire project; the user may specify a file or directory instead.
When invoked:
Detect the project's Go version from go.mod (the go directive).
Find all .go files in the target scope (excluding vendor/, .git/, testdata/).
For each file, apply all transformations for versions ≤ the project's Go version, starting from the oldest to the newest.
After all transformations, print a summary of what was changed and what was skipped.
If the user specifies a file or directory, limit the scope to that path.
Transformation Catalog
Each transformation includes a Go version gate—only apply when the project's go.mod version ≥ that version. Never apply a transformation that requires a version higher than the project declares.
// before//go:build linux && amd64// +build linux,amd64packagefoo// after//go:build linux && amd64packagefoo
The plusbuild modernizer removes obsolete // +build constraint lines once the equivalent //go:build line is present (the //go:build syntax landed in Go 1.17). Only strip the old line when a matching //go:build already exists — never drop the sole constraint.
Go 1.17+ — unsafe.Add / unsafe.Slice (unsafefuncs)
Before
After
unsafe.Pointer(uintptr(ptr) + uintptr(n))
unsafe.Add(ptr, n)
(*[n]T)(unsafe.Pointer(p))[:] slice construction
unsafe.Slice(p, n)
// before — pointer arithmetic via uintptrp2:=unsafe.Pointer(uintptr(ptr)+uintptr(offset))// afterp2:=unsafe.Add(ptr,offset)
// before — building a slice from a base pointers:=(*[1<<30]byte)(unsafe.Pointer(p))[:n:n]// afters:=unsafe.Slice(p,n)
The unsafefuncs modernizer (gopls v0.22.0) rewrites error-prone uintptr pointer math into unsafe.Add / unsafe.Slice, which the compiler and go vet understand as GC-safe.
The atomictypes modernizer (gopls v0.22.0, AtomicTypesAnalyzer) rewrites both the variable declaration and every call site. Typed wrappers (atomic.Int32/Int64/Uint32/Uint64/Bool/Pointer[T]) have identical performance but prevent accidental non-atomic access and fix 64-bit alignment crashes on 32-bit architectures.
Go 1.20+ — strings.Clone
Before
After
string([]byte(s))
strings.Clone(s)
// befores2:=string([]byte(s))// force copy// afters2:=strings.Clone(s)
// before → after: slices.Contains(items, target)found:=falsefor_,x:=rangeitems{ifx==target{found=truebreak}}
// before → after: slices.Index(items, target)fori,x:=rangeitems{ifx==target{returni}}return-1
// before → after: slices.SortFunc(items, cmp.Compare)sort.Slice(items,func(i,jint)bool{returnitems[i]<items[j]})
// before → after: slices.Max(items) / slices.Min(items)max:=items[0]for_,v:=rangeitems[1:]{ifv>max{max=v}}
// before → after: slices.Reverse(s)fori,j:=0,len(s)-1;i<j;i,j=i+1,j-1{s[i],s[j]=s[j],s[i]}
// before → after: slices.Compact(s)i:=0forj:=1;j<len(s);j++{ifs[j]!=s[i]{i++s[i]=s[j]}}s=s[:i+1]
// before → after: slices.Clip(s)s=s[:len(s):len(s)]
// before → after: slices.Clone(src)dst:=make([]T,len(src))copy(dst,src)
Requires importing "slices" and "cmp" (for SortFunc).
Go 1.21+ — slices.Delete / slices.Insert
Before
After
append(s[:i], s[i+1:]...)
slices.Delete(s, i, i+1)
append(s[:i:i], append([]T{x}, s[i:]...)...)
slices.Insert(s, i, x)
// before — element removal (classic aliasing/leak footgun)s=append(s[:i],s[i+1:]...)// afters=slices.Delete(s,i,i+1)
// before — insert at index is=append(s[:i],append([]T{v},s[i:]...)...)// afters=slices.Insert(s,i,v)
slices.Delete zeroes the tail elements to avoid retaining pointers (the manual append form leaks). Requires importing "slices".
Go 1.21+ — slices.Equal / maps.Equal
Before
After
reflect.DeepEqual(a, b) for comparable slices
slices.Equal(a, b)
reflect.DeepEqual(m1, m2) for comparable maps
maps.Equal(m1, m2)
// beforeifreflect.DeepEqual(got,want){...}// got, want are []string// afterifslices.Equal(got,want){...}
Faster and type-safe, with no reflection. Only for element types that are directly comparable (use slices.EqualFunc / maps.EqualFunc otherwise). Requires importing "slices" or "maps".
Go 1.21+ — maps package
Before
After
Manual loop to copy a map
maps.Clone(m)
for k, v := range src { dst[k] = v }
maps.Copy(dst, src)
Loop + conditional delete
maps.DeleteFunc(m, predicate)
// before → after: maps.Clone(m)dst:=make(map[K]V)fork,v:=rangesrc{dst[k]=v}
// before → after: maps.Copy(dst, src)fork,v:=rangesrc{dst[k]=v}
// before → after: maps.DeleteFunc(m, func(k K, v V) bool { return v == 0 })fork,v:=rangem{ifv==0{delete(m,k)}}
// before — three-way concatmerged:=append(append(append([]string(nil),x...),y...),z...)// aftermerged:=slices.Concat(x,y,z)
The appendclipped modernizer replaces nested append concatenation of multiple slices with slices.Concat, which allocates a fresh, correctly-sized result. slices.Concat was added in Go 1.22. Requires importing "slices". Only apply when the pattern builds a new slice (starts from []T(nil) or a clipped base) — not when it appends in place to an existing slice.
// before — type-specific boundd:=time.Duration(rand.Int63n(int64(max)))// after — generic N works for any integer typed:=rand.N(max)
math/rand/v2 (Go 1.22) drops the deprecated global Seed (top-level funcs are auto-seeded) and adds generic rand.N[T]. Semantic migration: the random stream differs from math/rand, so do not apply where reproducibility from a fixed seed matters. Flag as a suggestion, not auto-apply.
// before — callback-based iterationfunc(t*Tree)Each(fnfunc(vint)){for_,v:=ranget.values{fn(v)}}// caller: t.Each(func(v int) { use(v) })// after — standard iterator, usable with rangefunc(t*Tree)All()iter.Seq[int]{returnfunc(yieldfunc(int)bool){for_,v:=ranget.values{if!yield(v){return}}}}// caller: for v := range t.All() { use(v) }
Adopt the iter.Seq[T] / iter.Seq2[K,V] protocol (Go 1.23) so custom containers compose with range, slices.Collect, maps.Keys, etc. Requires importing "iter". Flag as a suggestion — it reshapes the API surface.
Go 1.23+ — Iterator helpers
Before
After
var keys []K; for k := range m { keys = append(keys, k) }
slices.Collect(maps.Keys(m))
var vals []V; for _, v := range m { vals = append(vals, v) }
// before — index still neededfori:=len(items)-1;i>=0;i--{fmt.Println(i,items[i])}// afterfori,v:=rangeslices.Backward(items){fmt.Println(i,v)}
The slicesbackward modernizer (gopls v0.22.0) replaces manual descending-index loops with the slices.Backward iterator. Requires importing "slices". Caveat: the rewrite preserves exact semantics in normal cases, but do not apply it when the loop body mutates the slice length or the index is used for out-of-band arithmetic — those edge cases can become unsound.
strings.Lines / bytes.Lines (Go 1.24) return line iterators — no Scanner setup, no default 64KB token-size limit. Note the yielded line retains its trailing \n, unlike Scanner.Text(); trim it if the old code relied on stripped lines. Only apply for in-memory strings/buffers, not streaming io.Readers.
Go 1.24+ — os.Root (directory-scoped filesystem access)
Before
After
manual filepath.Clean + prefix check to block traversal
root, _ := os.OpenRoot(dir); root.Open(name)
// before — hand-rolled path-traversal guardp:=filepath.Join(base,name)if!strings.HasPrefix(filepath.Clean(p),filepath.Clean(base)+string(os.PathSeparator)){returnerrUnsafePath}f,err:=os.Open(p)// after — the OS enforces the boundaryroot,err:=os.OpenRoot(base)iferr!=nil{returnerr}deferroot.Close()f,err:=root.Open(name)// symlinks/".." escaping base are rejected
os.Root (Go 1.24) confines all operations to a directory tree, rejecting .. and symlink escapes at the syscall layer — far more robust than string prefix checks. Security hardening: flag as a strong suggestion wherever user-controlled paths are joined to a base directory.
The embedlit modernizer (gopls v0.22.0, EmbedLitAnalyzer) strips redundant embedded-struct field-type specifiers from composite literals. Go 1.27 lets you initialize promoted fields directly without the nested literal. Only apply when the promoted field names don't collide with the outer struct's own fields.
Operation Phases
Phase 1: Detect
Read go.mod to extract the Go version (go 1.xx line). If no go.mod is found, default to go 1.21.
Phase 2: Gather files
Find all .go files in the target scope (project root, or user-specified file/directory). Exclude vendor/, .git/, and testdata/ directories.
Phase 3: Apply transformations
For each .go file, apply all transformations for versions ≤ the detected Go version. Process files sequentially. For each file:
Read the file content.
Identify applicable transformations by scanning for the "Before" patterns.
Apply each transformation using the Edit tool.
Run goimports -w (or gofmt -w) on the file after all edits.
Never apply a transformation that requires a version higher than the project's Go version.
Phase 4: Report
Print a summary table showing:
File: path relative to project root
Transformations applied: list of transformation names per file
Total files modified and total transformations applied
Skipped transformations (available but not applicable due to version constraints) and their required Go version
Example Summary Output
## Modernization Summary
| File | Transformations |
|---|---|
| main.go | any, strings.Cut, min/max (2 occurrences) |
| pkg/handler.go | range over int (3), slices.Contains, t.Context() |
| pkg/util.go | new(expr) (1), errors.AsType → errors.Is |
**3 files modified, 10 transformations applied**
Skipped (requires higher Go version):
- new(expr): requires go 1.26 (project is go 1.24)
- WaitGroup.Go: requires go 1.25 (project is go 1.24)
Safety Rules
Never apply transformations that change semantics in edge cases without the user's awareness.
Do not apply omitzero blindly—it changes JSON serialization behavior; flag it as a suggestion instead.
Treat semantic migrations as suggestions, not auto-applies: math/rand/v2 (changes the random stream), iter.Seq iterators (reshapes the API), os.Root (behavior/error-path change). Point them out and let the user opt in.
When replacing a bufio.Scanner loop with strings.Lines/bytes.Lines, remember the yielded line keeps its trailing \n; add a TrimSuffix if the old code relied on Scanner.Text() semantics.
Do not apply strings.SplitSeq or bytes.SplitSeq when the loop body references the index or the full slice elsewhere.
Do not apply strings.Builder if the concatenation happens outside a loop (single += is fine).
When a transformation requires a new import, ensure the import is added to the file.
After all edits, run goimports -w on each modified file to clean up imports.
If goimports is not available, fall back to gofmt -w.
If the project has no go.mod, ask the user for the target Go version before proceeding.
Automated tooling (Go 1.27+)
Many of these transformations are now shipped as official modernizers in gopls/go fix. On Go 1.27+ the whole set can be applied across a module with:
go fix ./...
gopls v0.22.0 added four notable passes covered above:
Other modernizers in the same suite that this catalog covers: minmax, efaceany, fmtappendf, stringscut, stringsseq, sortslice/slicescontains, mapsloop, stditerators, forvar, rangeint, testingcontext, bloop, waitgroup, newexpr, errorsastype, appendclipped (→ slices.Concat), and plusbuild (→ drop obsolete // +build).
To disable an over-eager pass (e.g. slicesbackward rewriting loops that mutate the slice), scope the run with -fixes or exclude that analyzer in your editor's gopls settings.