modern-go

Skill file

Preview skill file
---
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:
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
// before
elapsed := time.Now().Sub(start)
// after
elapsed := time.Since(start)
```

### Go 1.8+ — `time.Until`

| Before | After |
|---|---|
| `deadline.Sub(time.Now())` | `time.Until(deadline)` |

```go
// before
remaining := deadline.Sub(time.Now())
// after
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)` |

```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+ — `errors.Is`

| Before | After |
|---|---|
| `err == io.EOF` | `errors.Is(err, io.EOF)` |

```go
// before
if err == io.EOF {
    return
}
// after
if errors.Is(err, io.EOF) {
    return
}
```

### Go 1.18+ — `any`

| Before | After |
|---|---|
| `interface{}` | `any` |

```go
// before
func decode(v interface{}) error { ... }
// after
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)` |

```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+ — `bytes.Cut`

| 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+ — `fmt.Appendf`

| 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 |
|---|---|
| `atomic.StoreInt32(&v, 1)` / `atomic.LoadInt32(&v)` | `var v atomic.Int32; v.Store(1); v.Load()` |
| `atomic.Value` + type assertion | `atomic.Pointer[T]` |

```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+ — `strings.Clone`

| Before | After |
|---|---|
| `string([]byte(s))` | `strings.Clone(s)` |

```go
// before
s2 := string([]byte(s)) // force copy
// after
s2 := strings.Clone(s)
```

### Go 1.20+ — `bytes.Clone`

| Before | After |
|---|---|
| `make([]byte, len(src)); copy(dst, src)` | `bytes.Clone(src)` |

```go
// before
dst := make([]byte, len(src))
copy(dst, src)
// after
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 }` |

```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+ — `errors.Join`

| Before | After |
|---|---|
| `fmt.Errorf("...: %w: %w", err1, err2)` | `errors.Join(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+ — `context.WithCancelCause`

| Before | After |
|---|---|
| `ctx, cancel := context.WithCancel(parent)` + bare `cancel()` | `ctx, cancel := context.WithCancelCause(parent)` + `cancel(err)` |

```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+ — `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
// 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+ — `clear`

| Before | After |
|---|---|
| `for k := range m { delete(m, k) }` | `clear(m)` |
| `for i := range s { s[i] = zero }` | `clear(s)` |

```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+ — `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)` |

```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 `"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)` |

```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)
    }
}
```

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 })` |

```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+ — `context.AfterFunc`

| 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+ — `context.WithTimeoutCause` / `WithDeadlineCause`

| 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 := range n { ... }` |
| `for i := 0; i < n; i++ { ... }` (i unused) | `for range n { ... }` |

```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 `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, ...)` |

```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")
```

Requires importing `"cmp"`.

### Go 1.22+ — `reflect.TypeFor`

| Before | After |
|---|---|
| `reflect.TypeOf((*T)(nil)).Elem()` | `reflect.TypeFor[T]()` |

```go
// before
t := reflect.TypeOf((*MyType)(nil)).Elem()
// after
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")` |

```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))
```

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)` |

```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+ — `bytes.SplitSeq` / `bytes.FieldsSeq`

| 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+ — `t.Context()` in tests

| Before | After |
|---|---|
| `ctx, cancel := context.WithCancel(context.Background()); defer cancel()` | `ctx := t.Context()` |

```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+ — `omitzero` struct tag

| Before | After |
|---|---|
| `json:"field,omitempty"` (for `time.Time`, `time.Duration`, structs, slices, maps) | `json:"field,omitzero"` |

```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 `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() { ... }` |

```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+ — `sync.WaitGroup.Go`

| Before | After |
|---|---|
| `wg.Add(1); go func() { defer wg.Done(); fn() }()` | `wg.Go(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+ — `new` with expressions

| Before | After |
|---|---|
| `v := val; &v` | `new(val)` |
| Helper `func ptr[T any](v T) *T { return &v }` | `new(val)` 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+ — `errors.AsType`

| 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 `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:
1. Read the file content.
2. Identify applicable transformations by scanning for the "Before" patterns.
3. Apply each transformation using the Edit tool.
4. 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.

Source

Creator's repository · smallnest/goal-workflow

View on GitHub

Security

Security checks in progress
Results will appear here once audits complete
Checked by 3 independent security firms
Does it try to trick the AI?Not yet checkedPending · Gen Agent Trust Hub
Does it sneak in hidden code?Not yet checkedPending · Socket
Does it have known bugs?Not yet checkedPending · Snyk