Now liveThe Skillselion MCP - thousands of ranked skills, loaded into your agent mid-task. No install.Get it →
smallnest avatar

Modern Go

  • 325 installs
  • 199 repo stars
  • Updated July 21, 2026
  • smallnest/goal-workflow

Helps with ai & agent building tasks.

About

modern-go is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.

  • modern-go
  • AI & Agent Building
  • AI-coding skill

Modern Go by the numbers

  • 325 all-time installs (skills.sh)
  • +35 installs in the week ending Aug 5, 2026 (Skillselion tracking)
  • Ranked #2,216 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
  • Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/smallnest/goal-workflow --skill modern-go

Add your badge

Show developers this skill is listed on Skillselion. Paste this into your README.

Listed on Skillselion
Installs325
repo stars199
Last updatedJuly 21, 2026
Repositorysmallnest/goal-workflow

What it does

Helps with ai & agent building tasks.

Files

SKILL.mdMarkdownGitHub ↗

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

BeforeAfter
time.Now().Sub(start)time.Since(start)
// before
elapsed := time.Now().Sub(start)
// after
elapsed := time.Since(start)

Go 1.8+ — time.Until

BeforeAfter
deadline.Sub(time.Now())time.Until(deadline)
// before
remaining := deadline.Sub(time.Now())
// after
remaining := time.Until(deadline)

Go 1.10+ — strings.Builder (loop concatenation)

BeforeAfter
s += item in a loopvar b strings.Builder; b.WriteString(item)
// 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

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

Go 1.18+ — any

BeforeAfter
interface{}any
// before
func decode(v interface{}) error { ... }
// after
func decode(v any) error { ... }

Go 1.18+ — strings.Cut

BeforeAfter
i := strings.Index(s, sep); ... s[:i], s[i+len(sep):]key, val, found := strings.Cut(s, sep)
// 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

BeforeAfter
i := bytes.Index(b, sep); ... b[:i], b[i+len(sep):]before, after, found := bytes.Cut(b, sep)
// 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

BeforeAfter
buf = append(buf, fmt.Sprintf(...)...)buf = fmt.Appendf(buf, ...)
// before
buf = append(buf, fmt.Sprintf("x=%d", x)...)
// after
buf = fmt.Appendf(buf, "x=%d", x)

Go 1.19+ — Type-safe atomics

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

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

Go 1.20+ — bytes.Clone

BeforeAfter
make([]byte, len(src)); copy(dst, src)bytes.Clone(src)
// before
dst := make([]byte, len(src))
copy(dst, src)
// after
dst := bytes.Clone(src)

Go 1.20+ — strings.CutPrefix / strings.CutSuffix

BeforeAfter
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 }
// before
if strings.HasPrefix(s, "pre_") {
    s = s[len("pre_"):]
}
// after
if rest, ok := strings.CutPrefix(s, "pre_"); ok {
    s = rest
}
// 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

BeforeAfter
fmt.Errorf("...: %w: %w", err1, err2)errors.Join(err1, err2)
// 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

BeforeAfter
ctx, cancel := context.WithCancel(parent) + bare cancel()ctx, cancel := context.WithCancelCause(parent) + cancel(err)
// 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

BeforeAfter
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)
// before
lo := a
if b < lo {
    lo = b
}
// after
lo := min(a, b)
// before
if x < 0 {
    x = 0
}
if x > 100 {
    x = 100
}
// after
x = min(max(x, 0), 100)

Go 1.21+ — clear

BeforeAfter
for k := range m { delete(m, k) }clear(m)
for i := range s { s[i] = zero }clear(s)
// before
for k := range m {
    delete(m, k)
}
// after
clear(m)
// before
for i := range s {
    s[i] = 0
}
// after
clear(s)

Go 1.21+ — slices package

BeforeAfter
Manual loop to find elementslices.Contains(items, target)
Loop returning index or -1slices.Index(items, target)
sort.Slice(items, func(i,j int) bool { return items[i] < items[j] })slices.SortFunc(items, cmp.Compare)
Max/min finding loopslices.Max(items) / slices.Min(items)
Reverse swap loopslices.Reverse(s)
Remove consecutive duplicates loopslices.Compact(s)
s[:len(s):len(s)]slices.Clip(s)
make([]T, len(src)); copy(dst, src)slices.Clone(src)
// before → after: slices.Contains(items, target)
found := false
for _, x := range items {
    if x == target {
        found = true
        break
    }
}
// before → after: slices.Index(items, target)
for i, x := range items {
    if x == target {
        return i
    }
}
return -1
// before → after: slices.SortFunc(items, cmp.Compare)
sort.Slice(items, func(i, j int) bool { return items[i] < items[j] })
// before → after: slices.Max(items) / slices.Min(items)
max := items[0]
for _, v := range items[1:] {
    if v > max {
        max = v
    }
}
// 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]
}
// 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]
// 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+ — maps package

BeforeAfter
Manual loop to copy a mapmaps.Clone(m)
for k, v := range src { dst[k] = v }maps.Copy(dst, src)
Loop + conditional deletemaps.DeleteFunc(m, predicate)
// before → after: maps.Clone(m)
dst := make(map[K]V)
for k, v := range src {
    dst[k] = v
}
// before → after: maps.Copy(dst, src)
for k, v := range src {
    dst[k] = v
}
// 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

BeforeAfter
var once sync.Once; once.Do(func() { ... })f := sync.OnceFunc(func() { ... }); f()
sync.Once + stored result variablesync.OnceValue(func() T { return val })
// before
var once sync.Once
func init() { once.Do(func() { setup() }) }

// after
var initOnce = sync.OnceFunc(func() { setup() })
// 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

BeforeAfter
go func() { <-ctx.Done(); cleanup() }()stop := context.AfterFunc(ctx, cleanup)
// before
go func() {
    <-ctx.Done()
    conn.Close()
}()
// after
stop := context.AfterFunc(ctx, func() { conn.Close() })

Go 1.21+ — context.WithTimeoutCause / WithDeadlineCause

BeforeAfter
context.WithTimeout(parent, d)context.WithTimeoutCause(parent, d, err)
// 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

BeforeAfter
for i := 0; i < n; i++ { ... }for i := range n { ... }
for i := 0; i < n; i++ { ... } (i unused)for range n { ... }
// before
for i := 0; i < len(items); i++ {
    process(i, items[i])
}
// after
for i := range len(items) {
    process(i, items[i])
}
// before
for i := 0; i < n; i++ {
    doWork()
}
// after
for range n {
    doWork()
}

Go 1.22+ — Loop variable shadowing removal

BeforeAfter
for _, x := range items { x := x; ... }for _, x := range items { ... }
// 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

BeforeAfter
Chain of if v == "" { v = fallback }v := cmp.Or(val, fallback1, fallback2, ...)
// 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

BeforeAfter
reflect.TypeOf((*T)(nil)).Elem()reflect.TypeFor[T]()
// before
t := reflect.TypeOf((*MyType)(nil)).Elem()
// after
t := reflect.TypeFor[MyType]()

Go 1.22+ — Enhanced http.ServeMux

BeforeAfter
mux.HandleFunc("/api/", h) + manual path parsingmux.HandleFunc("GET /api/{id}", h) + r.PathValue("id")
// 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

BeforeAfter
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))
// before
var keys []string
for k := range m {
    keys = append(keys, k)
}
// after
keys := slices.Collect(maps.Keys(m))
// 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

BeforeAfter
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)
// 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

BeforeAfter
for _, part := range bytes.Split(b, sep)for part := range bytes.SplitSeq(b, sep)
// 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

BeforeAfter
ctx, cancel := context.WithCancel(context.Background()); defer cancel()ctx := t.Context()
// 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

BeforeAfter
json:"field,omitempty" (for time.Time, time.Duration, structs, slices, maps)json:"field,omitzero"
// 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

BeforeAfter
for i := 0; i < b.N; i++ { ... }for b.Loop() { ... }
// 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

BeforeAfter
wg.Add(1); go func() { defer wg.Done(); fn() }()wg.Go(fn)
// 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

BeforeAfter
v := val; &vnew(val)
Helper func ptr[T any](v T) *T { return &v }new(val) directly
// before
timeout := 30
debug := true
cfg := Config{
    Timeout: &timeout,
    Debug:   &debug,
}
// after
cfg := Config{
    Timeout: new(30),
    Debug:   new(true),
}
// 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

BeforeAfter
var t *T; errors.As(err, &t)t, ok := errors.AsType[*T](err)
// 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.

Related skills

This week in AI coding

Five minutes, every Monday - the tools, releases and tactics for developers.

unsubscribe anytime.