modern-go
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", "up
By smallnest · 436 installs
npx skills add smallnest/goal-workflow --skill modern-go
Source repository · Upstream listing
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:
1. Detect the project's Go version from go.mod (the go directive).
2. Find all .go files in the target scope (excluding vendor/ , .git/ , testdata/ ).
3. For each file, apply all transformations for versions ≤ the project's Go version , starting from the oldest to the newest.
4. 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.
Go 1.0+ — time.Since
Before After
time.Now().Sub(start) time.Since(start)
Go 1.8+ — time.Until
Before After
deadline.Sub(time.Now()) time.Until(deadline)
Go 1.10+ — strings.Builder (loop concatenation)
Before After
s += item in a loop var b strings.Builder; b.WriteString(item)
Only when += concatenation happens inside a loop.
Go 1.13+ — errors.Is
Before After
err == io.EOF errors.Is(err, io.EOF)
Go 1.17+ — //go:build constraints (plusbuild)
Before After
// +build linux + //go:build linux (both present) keep only //go:build linux
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)
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.
Go 1.18+ — any
Before After
interface{} any
Go 1.18+ — strings.Cut
Before After
i := strings.Index(s, sep); ... s[:i], s[i+len(sep):] key, val, found := strings.Cut(s, sep)
Go 1.18+ — bytes.Cut
Before After
i := bytes.Index(b, sep); ... b[:i], b[i+len(sep):] before, after, found := bytes.Cut(b, sep)
Go 1.19+ — fmt.Appendf
Before After
buf = append(buf, fmt.Sprintf(...)...) buf = fmt.Appendf(buf, ...)
Go 1.19+ — Type safe atomics (atomictypes)
Before After
atomic.StoreInt32(&v, 1) / atomic.LoadInt32(&v) var v atomic.Int32; v.Store(1); v.Load()
atomic.AddInt64(&v, 1) var v atomic.Int64; v.Add(1)
atomic.Value + type assertion atomic.Pointer[T]
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)
Go 1.20+ — bytes.Clone
Before After
make([]byte, len(src)); copy(dst, src) bytes.Clone(src)
Go 1.20+ — strings.CutPrefix / strings.CutSuffix
Before After
if strings.HasPrefix(s, p) { s = s[len(p):] } if rest, ok := strings.CutPrefix(s, p); ok { s = rest }
if strings.HasSuffix(s, sf) { s = s[:len(s) len(sf)] } if rest, ok := strings.CutSuffix(s, sf); ok { s = rest }
Go 1.20+ — errors.Join
Before After
fmt.Errorf("...: %w: %w", err1, err2) errors.Join(err1, err2)
Go 1.20+ — context.WithCancelCause
Before After
ctx, cancel := context.WithCancel(parent) + bare cancel() ctx, cancel := context.WithCancelCause(parent) + cancel(err)
Go 1.21+ — min / max
Before After
if a < b { v = a } else { v = b } v = min(a, b)
if a b { v = a } else { v = b } v = max(a, b)
if x < lo { x = lo }; if x hi { x = hi } x = min(max(x, lo), hi)
Go 1.21+ — clear
Before After
for k := range m { delete(m, k) } clear(m)
for i := range s { s[i] = zero } clear(s)
Go 1.21+ — slices package
Before After
Manual loop to find element slices.Contains(items, target)
Loop returning index or 1 slices.Index(items, target)
sort.Slice(items, func(i,j int) bool { return items[i] < items[j] }) slices.SortFunc(items, cmp.Compare)
Max/min finding loop slices.Max(items) / slices.Min(items)
Reverse swap loop slices.Reverse(s)
Remove consecutive duplicates loop slices.Compact(s)
s[:len(s):len(s)] slices.Clip(s)
make([]T, len(src)); copy(dst, src) slices.Clone(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)
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)
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)
Requires importing "maps" .
Go 1.22+ — slices.Concat (appendclipped)
Before After
append(append([]T(nil), s1...), s2...) slices.Concat(s1, s2)
append(slices.Clip(s1), s2...) for a fresh result slices.Concat(s1, s2)
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.
Go 1.21+ — sync.OnceFunc / sync.OnceValue
Before After
var once sync.Once; once.Do(func() { ... }) f := sync.OnceFunc(func() { ... }); f()
sync.Once + stored result variable sync.OnceValue(func() T { return val })
Go 1.21+ — context.AfterFunc
Before After
go func() { < ctx.Done(); cleanup() }() stop := context.AfterFunc(ctx, cleanup)
Go 1.21+ — context.WithTimeoutCause / WithDeadlineCause
Before After
context.WithTimeout(parent, d) context.WithTimeoutCause(parent, d, err)
Only apply when a meaningful cause error is available.
Go 1.22+ — Range over integer
Before After
for i := 0; i < n; i++ { ... } for i := range n { ... }
for i := 0; i < n; i++ { ... } (i unused) for range n { ... }
Go 1.22+ — Loop variable shadowing removal
Before After
for , x := range items { x := x; ... } for , x := range items { ... }
The x := x capture idiom is redundant since Go 1.22.
Go 1.22+ — cmp.Or
Before After
Chain of if v == "" { v = fallback } v := cmp.Or(val, fallback1, fallback2, ...)
Requires importing "cmp" .
Go 1.22+ — reflect.TypeFor
Before After
reflect.TypeOf(( T)(nil)).Elem() reflect.TypeFor[T]()
Go 1.22+ — Enhanced http.ServeMux
Before After
mux.HandleFunc("/api/", h) + manual path parsing mux.HandleFunc("GET /api/{id}", h) + r.PathValue("id")
Go 1.22+ — math/rand/v2
Before After
rand.Intn(n) rand.IntN(n)
rand.Int63n(n) / rand.Int31n(n) rand.Int64N(n) / rand.Int32N(n)
rand.Seed(...) + global funcs drop Seed ; use auto seeded top level funcs or rand.N[T]
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.
Go 1.23+ — Range over function (iterators)
Before After
Custom func Walk(yield func(T) bool) callback traversal func Walk() iter.Seq[T] returning an iterator
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) } slices.Collect(maps.Values(m))
Requires importing "slices" and "maps" .
Go 1.23+ — strings.SplitSeq / strings.FieldsSeq
Before After
for , part := range strings.Split(s, sep) for part := range strings.SplitSeq(s, sep)
for , field := range strings.Fields(s) for field := range strings.FieldsSeq(s)
Only when the loop body does not need the index or the full slice.
Go 1.23+ — bytes.SplitSeq / bytes.FieldsSeq
Before After
for , part := range bytes.Split(b, sep) for part := range bytes.SplitSeq(b, sep)
Go 1.23+ — slices.Backward (slicesbackward)
Before After
for i := len(s) 1; i = 0; i { use(s[i]) } for , v := range slices.Backward(s) { use(v) }
reverse loop needing the index too for i, v := range slices.Backward(s) { ... }
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.
Go 1.24+ — t.Context() in tests
Before After
ctx, cancel := context.WithCancel(context.Background()); defer cancel() ctx := t.Context()
Go 1.24+ — strings.Lines / bytes.Lines
Before After
bufio.Scanner line loop over a string/buffer for line := range strings.Lines(s)
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.Reader s.
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)
os.Root (Go 1.24) confines all operations to a directory tree, rejecting .. and symlink escap