modern-go
Modernize Go source code by applying version-appropriate idioms, APIs, and language features. Works like
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 (the directive).
- Find all files in the target scope (excluding , , ).
- 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
version ≥ that version. Never apply a transformation that requires a version higher than the project declares.
Go 1.0+ —
go
// before
elapsed := time.Now().Sub(start)
// after
elapsed := time.Since(start)
Go 1.8+ —
go
// before
remaining := deadline.Sub(time.Now())
// after
remaining := time.Until(deadline)
Go 1.10+ — (loop concatenation)
| Before | After |
|---|
| in a loop | var b strings.Builder; b.WriteString(item)
|
go
// before
s := ""
for _, item := range items {
s += item
}
// after
var b strings.Builder
for _, item := range items {
b.WriteString(item)
}
s := b.String()
Only when
concatenation happens inside a loop.
Go 1.13+ —
go
// before
if err == io.EOF {
return
}
// after
if errors.Is(err, io.EOF) {
return
}
Go 1.18+ —
go
// before
func decode(v interface{}) error { ... }
// after
func decode(v any) error { ... }
Go 1.18+ —
| Before | After |
|---|
i := strings.Index(s, sep); ... s[:i], s[i+len(sep):]
| key, val, found := strings.Cut(s, sep)
|
go
// before
if i := strings.Index(s, "="); i >= 0 {
key, val := s[:i], s[i+1:]
}
// after
if key, val, found := strings.Cut(s, "="); found {
...
}
Go 1.18+ —
| Before | After |
|---|
i := bytes.Index(b, sep); ... b[:i], b[i+len(sep):]
| before, after, found := bytes.Cut(b, sep)
|
go
// before
if i := bytes.Index(b, sep); i >= 0 {
before, after := b[:i], b[i+len(sep):]
}
// after
before, after, found := bytes.Cut(b, sep)
Go 1.19+ —
| Before | After |
|---|
buf = append(buf, fmt.Sprintf(...)...)
| buf = fmt.Appendf(buf, ...)
|
go
// before
buf = append(buf, fmt.Sprintf("x=%d", x)...)
// after
buf = fmt.Appendf(buf, "x=%d", x)
Go 1.19+ — Type-safe atomics
| Before | After |
|---|
| / | var v atomic.Int32; v.Store(1); v.Load()
|
| + type assertion | |
go
// before
var ready int32
atomic.StoreInt32(&ready, 1)
if atomic.LoadInt32(&ready) == 1 { ... }
// after
var ready atomic.Int32
ready.Store(1)
if ready.Load() == 1 { ... }
go
// before
var cache atomic.Value
cache.Store(&Config{})
cfg := cache.Load().(*Config)
// after
var cache atomic.Pointer[Config]
cache.Store(&Config{})
cfg := cache.Load()
Go 1.20+ —
go
// before
s2 := string([]byte(s)) // force copy
// after
s2 := strings.Clone(s)
Go 1.20+ —
| Before | After |
|---|
make([]byte, len(src)); copy(dst, src)
| |
go
// before
dst := make([]byte, len(src))
copy(dst, src)
// after
dst := bytes.Clone(src)
Go 1.20+ — /
| 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
// before
if strings.HasPrefix(s, "pre_") {
s = s[len("pre_"):]
}
// after
if rest, ok := strings.CutPrefix(s, "pre_"); ok {
s = rest
}
go
// before
if strings.HasSuffix(s, ".txt") {
s = s[:len(s)-len(".txt")]
}
// after
if rest, ok := strings.CutSuffix(s, ".txt"); ok {
s = rest
}
Go 1.20+ —
| Before | After |
|---|
fmt.Errorf("...: %w: %w", err1, err2)
| |
go
// before
return fmt.Errorf("load config: %w: %w", err1, err2)
// after
return errors.Join(fmt.Errorf("load config"), err1, err2)
Go 1.20+ —
| Before | After |
|---|
ctx, cancel := context.WithCancel(parent)
+ bare | ctx, cancel := context.WithCancelCause(parent)
+ |
go
// before
ctx, cancel := context.WithCancel(parent)
// ... somewhere ...
cancel()
// after
ctx, cancel := context.WithCancelCause(parent)
cancel(ErrShutdown)
// caller: context.Cause(ctx) → ErrShutdown
Go 1.21+ — /
| Before | After |
|---|
if a < b { v = a } else { v = b }
| |
if a > b { v = a } else { v = b }
| |
if x < lo { x = lo }; if x > hi { x = hi }
| |
go
// before
lo := a
if b < lo {
lo = b
}
// after
lo := min(a, b)
go
// before
if x < 0 {
x = 0
}
if x > 100 {
x = 100
}
// after
x = min(max(x, 0), 100)
Go 1.21+ —
| Before | After |
|---|
for k := range m { delete(m, k) }
| |
for i := range s { s[i] = zero }
| |
go
// before
for k := range m {
delete(m, k)
}
// after
clear(m)
go
// before
for i := range s {
s[i] = 0
}
// after
clear(s)
Go 1.21+ — 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 | / |
| Reverse swap loop | |
| Remove consecutive duplicates loop | |
| |
make([]T, len(src)); copy(dst, src)
| |
go
// before → after: slices.Contains(items, target)
found := false
for _, x := range items {
if x == target {
found = true
break
}
}
go
// before → after: slices.Index(items, target)
for i, x := range items {
if x == target {
return i
}
}
return -1
go
// before → after: slices.SortFunc(items, cmp.Compare)
sort.Slice(items, func(i, j int) bool { return items[i] < items[j] })
go
// before → after: slices.Max(items) / slices.Min(items)
max := items[0]
for _, v := range items[1:] {
if v > max {
max = v
}
}
go
// before → after: slices.Reverse(s)
for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
s[i], s[j] = s[j], s[i]
}
go
// before → after: slices.Compact(s)
i := 0
for j := 1; j < len(s); j++ {
if s[j] != s[i] {
i++
s[i] = s[j]
}
}
s = s[:i+1]
go
// before → after: slices.Clip(s)
s = s[:len(s):len(s)]
go
// before → after: slices.Clone(src)
dst := make([]T, len(src))
copy(dst, src)
Requires importing
and
(for
).
Go 1.21+ — package
| Before | After |
|---|
| Manual loop to copy a map | |
for k, v := range src { dst[k] = v }
| |
| Loop + conditional delete | maps.DeleteFunc(m, predicate)
|
go
// before → after: maps.Clone(m)
dst := make(map[K]V)
for k, v := range src {
dst[k] = v
}
go
// before → after: maps.Copy(dst, src)
for k, v := range src {
dst[k] = v
}
go
// before → after: maps.DeleteFunc(m, func(k K, v V) bool { return v == 0 })
for k, v := range m {
if v == 0 {
delete(m, k)
}
}
Go 1.21+ — /
| Before | After |
|---|
var once sync.Once; once.Do(func() { ... })
| f := sync.OnceFunc(func() { ... }); f()
|
| + stored result variable | sync.OnceValue(func() T { return val })
|
go
// before
var once sync.Once
func init() { once.Do(func() { setup() }) }
// after
var initOnce = sync.OnceFunc(func() { setup() })
go
// before
var once sync.Once
var cfg *Config
func getConfig() *Config {
once.Do(func() { cfg = loadConfig() })
return cfg
}
// after
var getConfig = sync.OnceValue(func() *Config { return loadConfig() })
Go 1.21+ —
| Before | After |
|---|
go func() { <-ctx.Done(); cleanup() }()
| stop := context.AfterFunc(ctx, cleanup)
|
go
// before
go func() {
<-ctx.Done()
conn.Close()
}()
// after
stop := context.AfterFunc(ctx, func() { conn.Close() })
Go 1.21+ — /
| Before | After |
|---|
context.WithTimeout(parent, d)
| context.WithTimeoutCause(parent, d, err)
|
go
// before
ctx, cancel := context.WithTimeout(parent, 5*time.Second)
// after
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 := 0; i < n; i++ { ... }
(i unused) | |
go
// before
for i := 0; i < len(items); i++ {
process(i, items[i])
}
// after
for i := range len(items) {
process(i, items[i])
}
go
// before
for i := 0; i < n; i++ {
doWork()
}
// after
for range n {
doWork()
}
Go 1.22+ — Loop variable shadowing removal
| Before | After |
|---|
for _, x := range items { x := x; ... }
| for _, x := range items { ... }
|
go
// before
for _, x := range items {
x := x // capture for goroutine
go func() { use(x) }()
}
// after
for _, x := range items {
go func() { use(x) }()
}
The
capture idiom is redundant since Go 1.22.
Go 1.22+ —
| Before | After |
|---|
Chain of if v == "" { v = fallback }
| v := cmp.Or(val, fallback1, fallback2, ...)
|
go
// before
name := os.Getenv("NAME")
if name == "" {
name = os.Getenv("USER")
}
if name == "" {
name = "anonymous"
}
// after
name := cmp.Or(os.Getenv("NAME"), os.Getenv("USER"), "anonymous")
Go 1.22+ —
| Before | After |
|---|
reflect.TypeOf((*T)(nil)).Elem()
| |
go
// before
t := reflect.TypeOf((*MyType)(nil)).Elem()
// after
t := reflect.TypeFor[MyType]()
Go 1.22+ — Enhanced
| Before | After |
|---|
mux.HandleFunc("/api/", h)
+ manual path parsing | mux.HandleFunc("GET /api/{id}", h)
+ |
go
// before
mux.HandleFunc("/api/", func(w http.ResponseWriter, r *http.Request) {
id := strings.TrimPrefix(r.URL.Path, "/api/")
...
})
// after
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))
|
go
// before
var keys []string
for k := range m {
keys = append(keys, k)
}
// after
keys := slices.Collect(maps.Keys(m))
go
// before
var vals []int
for _, v := range m {
vals = append(vals, v)
}
// after
vals := slices.Collect(maps.Values(m))
Go 1.23+ — /
| 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)
|
go
// before
for _, part := range strings.Split(line, ",") {
process(part)
}
// after
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+ — /
| Before | After |
|---|
for _, part := range bytes.Split(b, sep)
| for part := range bytes.SplitSeq(b, sep)
|
go
// before
for _, part := range bytes.Split(data, sep) {
process(part)
}
// after
for part := range bytes.SplitSeq(data, sep) {
process(part)
}
Go 1.24+ — in tests
| Before | After |
|---|
ctx, cancel := context.WithCancel(context.Background()); defer cancel()
| |
go
// before
func TestFetch(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
result := fetch(ctx)
}
// after
func TestFetch(t *testing.T) {
result := fetch(t.Context())
}
Go 1.24+ — struct tag
| Before | After |
|---|
| (for , , structs, slices, maps) | |
go
// before
type Config struct {
Timeout time.Duration `json:"timeout,omitempty"`
Labels []string `json:"labels,omitempty"`
}
// after
type Config struct {
Timeout time.Duration `json:"timeout,omitzero"`
Labels []string `json:"labels,omitzero"`
}
Only for types where
fails:
,
, structs, slices, maps. Flag as suggestion, not auto-apply.
Go 1.24+ — in benchmarks
| Before | After |
|---|
for i := 0; i < b.N; i++ { ... }
| |
go
// before
func BenchmarkHash(b *testing.B) {
for i := 0; i < b.N; i++ {
hash(input)
}
}
// after
func BenchmarkHash(b *testing.B) {
for b.Loop() {
hash(input)
}
}
Go 1.25+ —
| Before | After |
|---|
wg.Add(1); go func() { defer wg.Done(); fn() }()
| |
go
// before
var wg sync.WaitGroup
for _, item := range items {
wg.Add(1)
go func(item Item) {
defer wg.Done()
process(item)
}(item)
}
wg.Wait()
// after
var wg sync.WaitGroup
for _, item := range items {
wg.Go(func() { process(item) })
}
wg.Wait()
Go 1.26+ — with expressions
| Before | After |
|---|
| |
Helper func ptr[T any](v T) *T { return &v }
| directly |
go
// before
timeout := 30
debug := true
cfg := Config{
Timeout: &timeout,
Debug: &debug,
}
// after
cfg := Config{
Timeout: new(30),
Debug: new(true),
}
go
// before
func ptr[T any](v T) *T { return &v }
cfg := Config{Count: ptr(10)}
// after
cfg := Config{Count: new(10)}
Go 1.26+ —
| Before | After |
|---|
var t *T; errors.As(err, &t)
| t, ok := errors.AsType[*T](err)
|
go
// before
var pathErr *os.PathError
if errors.As(err, &pathErr) {
log.Println(pathErr.Path)
}
// after
if pathErr, ok := errors.AsType[*os.PathError](err); ok {
log.Println(pathErr.Path)
}
Operation Phases
Phase 1: Detect
Read
to extract the Go version (
line). If no
is found, default to
.
Phase 2: Gather files
Find all
files in the target scope (project root, or user-specified file/directory). Exclude
,
, and
directories.
Phase 3: Apply transformations
For each
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 (or ) 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 blindly—it changes JSON serialization behavior; flag it as a suggestion instead.
- Do not apply or when the loop body references the index or the full slice elsewhere.
- Do not apply 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 on each modified file to clean up imports.
- If is not available, fall back to .
- If the project has no , ask the user for the target Go version before proceeding.