| name | modern-go |
| description | 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.
Go 1.0+ — time.Since
| Before | After |
|---|
time.Now().Sub(start) | time.Since(start) |
elapsed := time.Now().Sub(start)
elapsed := time.Since(start)
Go 1.8+ — time.Until
| Before | After |
|---|
deadline.Sub(time.Now()) | time.Until(deadline) |
remaining := deadline.Sub(time.Now())
remaining := time.Until(deadline)
Go 1.10+ — strings.Builder (loop concatenation)
| Before | After |
|---|
s += item in a loop | var b strings.Builder; b.WriteString(item) |
s := ""
for _, item := range items {
s += item
}
var b strings.Builder
for _, item := range items {
b.WriteString(item)
}
s := b.String()
Only when += concatenation happens inside a loop.
Go 1.13+ — errors.Is
| Before | After |
|---|
err == io.EOF | errors.Is(err, io.EOF) |
if err == io.EOF {
return
}
if errors.Is(err, io.EOF) {
return
}
Go 1.18+ — any
| Before | After |
|---|
interface{} | any |
func decode(v interface{}) error { ... }
func decode(v any) error { ... }
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) |
if i := strings.Index(s, "="); i >= 0 {
key, val := s[:i], s[i+1:]
}
if key, val, found := strings.Cut(s, "="); found {
...
}
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) |
if i := bytes.Index(b, sep); i >= 0 {
before, after := 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, ...) |
buf = append(buf, fmt.Sprintf("x=%d", x)...)
buf = fmt.Appendf(buf, "x=%d", x)
Go 1.19+ — Type-safe atomics
| Before | After |
|---|
atomic.StoreInt32(&v, 1) / atomic.LoadInt32(&v) | var v atomic.Int32; v.Store(1); v.Load() |
atomic.Value + type assertion | atomic.Pointer[T] |
var ready int32
atomic.StoreInt32(&ready, 1)
if atomic.LoadInt32(&ready) == 1 { ... }
var ready atomic.Int32
ready.Store(1)
if ready.Load() == 1 { ... }
var cache atomic.Value
cache.Store(&Config{})
cfg := cache.Load().(*Config)
var cache atomic.Pointer[Config]
cache.Store(&Config{})
cfg := cache.Load()
Go 1.20+ — strings.Clone
| Before | After |
|---|
string([]byte(s)) | strings.Clone(s) |
s2 := string([]byte(s))
s2 := strings.Clone(s)
Go 1.20+ — bytes.Clone
| Before | After |
|---|
make([]byte, len(src)); copy(dst, src) | bytes.Clone(src) |
dst := make([]byte, len(src))
copy(dst, src)
dst := 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 } |
if strings.HasPrefix(s, "pre_") {
s = s[len("pre_"):]
}
if rest, ok := strings.CutPrefix(s, "pre_"); ok {
s = rest
}
if strings.HasSuffix(s, ".txt") {
s = s[:len(s)-len(".txt")]
}
if rest, ok := strings.CutSuffix(s, ".txt"); ok {
s = rest
}
Go 1.20+ — errors.Join
| Before | After |
|---|
fmt.Errorf("...: %w: %w", err1, err2) | errors.Join(err1, err2) |
return fmt.Errorf("load config: %w: %w", err1, err2)
return errors.Join(fmt.Errorf("load config"), err1, err2)
Go 1.20+ — context.WithCancelCause
| Before | After |
|---|
ctx, cancel := context.WithCancel(parent) + bare cancel() | ctx, cancel := context.WithCancelCause(parent) + cancel(err) |
ctx, cancel := context.WithCancel(parent)
cancel()
ctx, cancel := context.WithCancelCause(parent)
cancel(ErrShutdown)
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) |
lo := a
if b < lo {
lo = b
}
lo := min(a, b)
if x < 0 {
x = 0
}
if x > 100 {
x = 100
}
x = min(max(x, 0), 100)
Go 1.21+ — clear
| Before | After |
|---|
for k := range m { delete(m, k) } | clear(m) |
for i := range s { s[i] = zero } | clear(s) |
for k := range m {
delete(m, k)
}
clear(m)
for i := range s {
s[i] = 0
}
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) |
found := false
for _, x := range items {
if x == target {
found = true
break
}
}
for i, x := range items {
if x == target {
return i
}
}
return -1
sort.Slice(items, func(i, j int) bool { return items[i] < items[j] })
max := items[0]
for _, v := range items[1:] {
if v > max {
max = v
}
}
for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
s[i], s[j] = s[j], s[i]
}
i := 0
for j := 1; j < len(s); j++ {
if s[j] != s[i] {
i++
s[i] = s[j]
}
}
s = s[:i+1]
s = s[:len(s):len(s)]
dst := make([]T, len(src))
copy(dst, src)
Requires importing "slices" and "cmp" (for SortFunc).
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) |
dst := make(map[K]V)
for k, v := range src {
dst[k] = v
}
for k, v := range src {
dst[k] = v
}
for k, v := range m {
if v == 0 {
delete(m, k)
}
}
Requires importing "maps".
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 }) |
var once sync.Once
func init() { once.Do(func() { setup() }) }
var initOnce = sync.OnceFunc(func() { setup() })
var once sync.Once
var cfg *Config
func getConfig() *Config {
once.Do(func() { cfg = loadConfig() })
return cfg
}
var getConfig = sync.OnceValue(func() *Config { return loadConfig() })
Go 1.21+ — context.AfterFunc
| Before | After |
|---|
go func() { <-ctx.Done(); cleanup() }() | stop := context.AfterFunc(ctx, cleanup) |
go func() {
<-ctx.Done()
conn.Close()
}()
stop := context.AfterFunc(ctx, func() { conn.Close() })
Go 1.21+ — context.WithTimeoutCause / WithDeadlineCause
| Before | After |
|---|
context.WithTimeout(parent, d) | context.WithTimeoutCause(parent, d, err) |
ctx, cancel := context.WithTimeout(parent, 5*time.Second)
ctx, cancel := context.WithTimeoutCause(parent, 5*time.Second, ErrTimeout)
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 { ... } |
for i := 0; i < len(items); i++ {
process(i, items[i])
}
for i := range len(items) {
process(i, items[i])
}
for i := 0; i < n; i++ {
doWork()
}
for range n {
doWork()
}
Go 1.22+ — Loop variable shadowing removal
| Before | After |
|---|
for _, x := range items { x := x; ... } | for _, x := range items { ... } |
for _, x := range items {
x := x
go func() { use(x) }()
}
for _, x := range items {
go func() { use(x) }()
}
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, ...) |
name := os.Getenv("NAME")
if name == "" {
name = os.Getenv("USER")
}
if name == "" {
name = "anonymous"
}
name := cmp.Or(os.Getenv("NAME"), os.Getenv("USER"), "anonymous")
Requires importing "cmp".
Go 1.22+ — reflect.TypeFor
| Before | After |
|---|
reflect.TypeOf((*T)(nil)).Elem() | reflect.TypeFor[T]() |
t := reflect.TypeOf((*MyType)(nil)).Elem()
t := reflect.TypeFor[MyType]()
Go 1.22+ — Enhanced http.ServeMux
| Before | After |
|---|
mux.HandleFunc("/api/", h) + manual path parsing | mux.HandleFunc("GET /api/{id}", h) + r.PathValue("id") |
mux.HandleFunc("/api/", func(w http.ResponseWriter, r *http.Request) {
id := strings.TrimPrefix(r.URL.Path, "/api/")
...
})
mux.HandleFunc("GET /api/{id}", func(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
...
})
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)) |
var keys []string
for k := range m {
keys = append(keys, k)
}
keys := slices.Collect(maps.Keys(m))
var vals []int
for _, v := range m {
vals = append(vals, v)
}
vals := 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) |
for _, part := range strings.Split(line, ",") {
process(part)
}
for part := range strings.SplitSeq(line, ",") {
process(part)
}
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) |
for _, part := range bytes.Split(data, sep) {
process(part)
}
for part := range bytes.SplitSeq(data, sep) {
process(part)
}
Go 1.24+ — t.Context() in tests
| Before | After |
|---|
ctx, cancel := context.WithCancel(context.Background()); defer cancel() | ctx := t.Context() |
func TestFetch(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
result := fetch(ctx)
}
func TestFetch(t *testing.T) {
result := fetch(t.Context())
}
Go 1.24+ — omitzero struct tag
| Before | After |
|---|
json:"field,omitempty" (for time.Time, time.Duration, structs, slices, maps) | json:"field,omitzero" |
type Config struct {
Timeout time.Duration `json:"timeout,omitempty"`
Labels []string `json:"labels,omitempty"`
}
type Config struct {
Timeout time.Duration `json:"timeout,omitzero"`
Labels []string `json:"labels,omitzero"`
}
Only for types where omitempty fails: time.Time, time.Duration, structs, slices, maps. Flag as suggestion, not auto-apply.
Go 1.24+ — b.Loop() in benchmarks
| Before | After |
|---|
for i := 0; i < b.N; i++ { ... } | for b.Loop() { ... } |
func BenchmarkHash(b *testing.B) {
for i := 0; i < b.N; i++ {
hash(input)
}
}
func BenchmarkHash(b *testing.B) {
for b.Loop() {
hash(input)
}
}
Go 1.25+ — sync.WaitGroup.Go
| Before | After |
|---|
wg.Add(1); go func() { defer wg.Done(); fn() }() | wg.Go(fn) |
var wg sync.WaitGroup
for _, item := range items {
wg.Add(1)
go func(item Item) {
defer wg.Done()
process(item)
}(item)
}
wg.Wait()
var wg sync.WaitGroup
for _, item := range items {
wg.Go(func() { process(item) })
}
wg.Wait()
Go 1.26+ — new with expressions
| Before | After |
|---|
v := val; &v | new(val) |
Helper func ptr[T any](v T) *T { return &v } | new(val) directly |
timeout := 30
debug := true
cfg := Config{
Timeout: &timeout,
Debug: &debug,
}
cfg := Config{
Timeout: new(30),
Debug: new(true),
}
func ptr[T any](v T) *T { return &v }
cfg := Config{Count: ptr(10)}
cfg := Config{Count: new(10)}
Go 1.26+ — errors.AsType
| Before | After |
|---|
var t *T; errors.As(err, &t) | t, ok := errors.AsType[*T](err) |
var pathErr *os.PathError
if errors.As(err, &pathErr) {
log.Println(pathErr.Path)
}
if pathErr, ok := errors.AsType[*os.PathError](err); ok {
log.Println(pathErr.Path)
}
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.
- 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.