Agent skill

Modern Go

by smallnest in smallnest/goal-workflow

Modernize Go code by applying version-appropriate idioms and APIs (gofix-style transformations).

MITAuto-check passedDevelopment

Install Modern Go

skills CLI
$ npx skills add smallnest/goal-workflow --skill modern-go -a claude-code

Project install by default; add -g for ~/.claude/skills/.

GitHub CLI
$ gh skill install smallnest/goal-workflow modern-go --agent claude-code

Project scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).

Manual copy
$ git clone --depth 1 https://github.com/smallnest/goal-workflow.git skills-src && mkdir -p .claude/skills && cp -r skills-src/skills/modern-go .claude/skills/modern-go && rm -rf skills-src

Use ~/.claude/skills/ instead of .claude/skills for a personal install. The folder must contain SKILL.md.

Claude Code skills documentation · loads skills from .claude/skills/

Facts

Skill name
modern-go
GitHub stars
290
Token cost
~7.2k tokens
SKILL.md length
1,960 words
Files
1
Skills in repo
20
Repo updated
First seen
Licence
MIT

At a glance

Modernize Go code by applying version-appropriate idioms and APIs (gofix-style transformations).

  • Works in 4 steps: Detect → Gather files → Apply transformations → …
  • The user says 现代化
  • SKILL.md covers Usage and Transformation Catalog
  • Calls go

What it does

Modern Go is an agent skill from smallnest/goal-workflow. 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.

Its SKILL.md is about 7.2k tokens, which your agent loads only when the skill is triggered. It is a single SKILL.md file with no bundled scripts.

It sits in Development. It works with Go and Linux. The repository describes itself as: AI-driven development workflow with /prd, /goal, /review-it and /ship-it skills. The licence is MIT.

When your agent uses it

  • The user says 现代化
  • Wants to upgrade Go idioms

Example prompts

  • “现代Go语言”
  • “idiomatic”
  • “modernize”
  • “/modern-go”

Workflow steps

4 steps, taken from the step headings in SKILL.md.

  1. Detect
  2. Gather files
  3. Apply transformations
  4. Report

What it can do on your machine

Read from SKILL.md and the folder at commit b06ab3c. It shows what the files ask for, not the result of running them.

  • Tool permissions

    Pre-approves nothing: there is no allowed-tools line, so your agent's usual permission prompts apply.

    From allowed-tools in the SKILL.md frontmatter.

  • Runs code

    Shell commands in SKILL.md call:

    • go

    From the folder's file list and the shell code blocks in SKILL.md.

  • Network

    No URLs in SKILL.md.

    From URLs in SKILL.md, links to its own repository left out.

  • Credentials

    Names no API keys, tokens, secrets or passwords.

    From names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.

Context cost

Modern Go loads about 7.2k tokens when it runs. Until then it costs about 91 tokens; SKILL.md has 1,960 words of instructions outside code blocks.

Always · name and description, kept in context so the agent knows when to use it
~91
When it runs · the whole SKILL.md, loaded when a task matches
~7.2k

Estimates: characters ÷ 4, the usual rule of thumb; real counts depend on the model's tokenizer. Scripts and assets cost tokens only if the agent reads them.

Safety

Auto-check passed

The automated check found no risky patterns in SKILL.md.

Automated static check — not a guarantee. Review scripts before installing. It scans the text of SKILL.md for risky patterns (piping downloads into a shell, reading credential files, hidden Unicode, destructive commands); files beside SKILL.md are not scanned.

SKILL.md

The full file from smallnest/goal-workflow at commit b06ab3c, republished under its MIT licence (© smallnest). 1,960 words, ~7,233 tokens.

Download SKILL.mdSave it as .claude/skills/modern-go/SKILL.md (or your agent's skills folder).
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
BeforeAfter
time.Now().Sub(start)time.Since(start)
go
// before
elapsed := time.Now().Sub(start)
// after
elapsed := time.Since(start)
Go 1.8+ — time.Until
BeforeAfter
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)
BeforeAfter
s += item in a loopvar 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
BeforeAfter
err == io.EOFerrors.Is(err, io.EOF)
go
// before
if err == io.EOF {
    return
}
// after
if errors.Is(err, io.EOF) {
    return
}
Go 1.17+ — //go:build constraints (plusbuild)
BeforeAfter
// +build linux + //go:build linux (both present)keep only //go:build linux
go
// before
//go:build linux && amd64
// +build linux,amd64

package foo
// after
//go:build linux && amd64

package foo

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)
BeforeAfter
unsafe.Pointer(uintptr(ptr) + uintptr(n))unsafe.Add(ptr, n)
(*[n]T)(unsafe.Pointer(p))[:] slice constructionunsafe.Slice(p, n)
go
// before — pointer arithmetic via uintptr
p2 := unsafe.Pointer(uintptr(ptr) + uintptr(offset))
// after
p2 := unsafe.Add(ptr, offset)
go
// before — building a slice from a base pointer
s := (*[1 << 30]byte)(unsafe.Pointer(p))[:n:n]
// after
s := 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
BeforeAfter
interface{}any
go
// 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)
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
BeforeAfter
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
BeforeAfter
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 (atomictypes)
BeforeAfter
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 assertionatomic.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()

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
BeforeAfter
string([]byte(s))strings.Clone(s)
go
// 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)
go
// 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 }
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
BeforeAfter
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
BeforeAfter
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
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)
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
BeforeAfter
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
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)
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+ — slices.Delete / slices.Insert
BeforeAfter
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)
go
// before — element removal (classic aliasing/leak footgun)
s = append(s[:i], s[i+1:]...)
// after
s = slices.Delete(s, i, i+1)
go
// before — insert at index i
s = append(s[:i], append([]T{v}, s[i:]...)...)
// after
s = slices.Insert(s, i, v)

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
BeforeAfter
reflect.DeepEqual(a, b) for comparable slicesslices.Equal(a, b)
reflect.DeepEqual(m1, m2) for comparable mapsmaps.Equal(m1, m2)
go
// before
if reflect.DeepEqual(got, want) { ... }   // got, want are []string
// after
if slices.Equal(got, want) { ... }

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
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)
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.22+ — slices.Concat (appendclipped)
BeforeAfter
append(append([]T(nil), s1...), s2...)slices.Concat(s1, s2)
append(slices.Clip(s1), s2...) for a fresh resultslices.Concat(s1, s2)
go
// before
all := append(append([]int(nil), a...), b...)
// after
all := slices.Concat(a, b)
go
// before — three-way concat
merged := append(append(append([]string(nil), x...), y...), z...)
// after
merged := slices.Concat(x, y, z)

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
BeforeAfter
var once sync.Once; once.Do(func() { ... })f := sync.OnceFunc(func() { ... }); f()
sync.Once + stored result variablesync.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
BeforeAfter
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
BeforeAfter
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
BeforeAfter
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
BeforeAfter
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
BeforeAfter
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
BeforeAfter
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
BeforeAfter
mux.HandleFunc("/api/", h) + manual path parsingmux.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.22+ — math/rand/v2
BeforeAfter
rand.Intn(n)rand.IntN(n)
rand.Int63n(n) / rand.Int31n(n)rand.Int64N(n) / rand.Int32N(n)
rand.Seed(...) + global funcsdrop Seed; use auto-seeded top-level funcs or rand.N[T]
go
// before
import "math/rand"
n := rand.Intn(100)
// after
import "math/rand/v2"
n := rand.IntN(100)
go
// before — type-specific bound
d := time.Duration(rand.Int63n(int64(max)))
// after — generic N works for any integer type
d := rand.N(max)

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)
BeforeAfter
Custom func Walk(yield func(T) bool) callback traversalfunc Walk() iter.Seq[T] returning an iterator
go
// before — callback-based iteration
func (t *Tree) Each(fn func(v int)) {
    for _, v := range t.values {
        fn(v)
    }
}
// caller: t.Each(func(v int) { use(v) })

// after — standard iterator, usable with range
func (t *Tree) All() iter.Seq[int] {
    return func(yield func(int) bool) {
        for _, v := range t.values {
            if !yield(v) { return }
        }
    }
}
// caller: for v := range t.All() { use(v) }

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
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))
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
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)
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
BeforeAfter
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.23+ — slices.Backward (slicesbackward)
BeforeAfter
for i := len(s) - 1; i >= 0; i-- { use(s[i]) }for _, v := range slices.Backward(s) { use(v) }
reverse loop needing the index toofor i, v := range slices.Backward(s) { ... }
go
// before
for i := len(items) - 1; i >= 0; i-- {
    process(items[i])
}
// after
for _, v := range slices.Backward(items) {
    process(v)
}
go
// before — index still needed
for i := len(items) - 1; i >= 0; i-- {
    fmt.Println(i, items[i])
}
// after
for i, v := range slices.Backward(items) {
    fmt.Println(i, v)
}

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.

Show full SKILL.md (749 more words)Show less
Go 1.24+ — t.Context() in tests
BeforeAfter
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+ — strings.Lines / bytes.Lines
BeforeAfter
bufio.Scanner line loop over a string/bufferfor line := range strings.Lines(s)
go
// before
sc := bufio.NewScanner(strings.NewReader(s))
for sc.Scan() {
    process(sc.Text())
}
// after
for line := range strings.Lines(s) {
    process(strings.TrimSuffix(line, "\n"))
}

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.Readers.

Go 1.24+ — os.Root (directory-scoped filesystem access)
BeforeAfter
manual filepath.Clean + prefix check to block traversalroot, _ := os.OpenRoot(dir); root.Open(name)
go
// before — hand-rolled path-traversal guard
p := filepath.Join(base, name)
if !strings.HasPrefix(filepath.Clean(p), filepath.Clean(base)+string(os.PathSeparator)) {
    return errUnsafePath
}
f, err := os.Open(p)
// after — the OS enforces the boundary
root, err := os.OpenRoot(base)
if err != nil { return err }
defer root.Close()
f, err := root.Open(name) // symlinks/".." escaping base are rejected

os.Root (Go 1.24) confines all operations to a directory tree, rejecting .. and symlink escapes at the syscall layer — far more robust than string prefix checks. Security hardening: flag as a strong suggestion wherever user-controlled paths are joined to a base directory.

Go 1.24+ — omitzero struct tag
BeforeAfter
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
BeforeAfter
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
BeforeAfter
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
BeforeAfter
v := val; &vnew(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
BeforeAfter
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)
}
Go 1.27+ — Embedded field literals (embedlit)
BeforeAfter
T{U: U{x: 1}} (redundant embedded-type specifier)T{x: 1}
go
type Base struct {
    ID   int
    Name string
}
type User struct {
    Base
    Age int
}

// before
u := User{
    Base: Base{ID: 1, Name: "alice"},
    Age:  30,
}
// after
u := User{
    ID:   1,
    Name: "alice",
    Age:  30,
}

The embedlit modernizer (gopls v0.22.0, EmbedLitAnalyzer) strips redundant embedded-struct field-type specifiers from composite literals. Go 1.27 lets you initialize promoted fields directly without the nested literal. Only apply when the promoted field names don't collide with the outer struct's own fields.

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.
  • Treat semantic migrations as suggestions, not auto-applies: math/rand/v2 (changes the random stream), iter.Seq iterators (reshapes the API), os.Root (behavior/error-path change). Point them out and let the user opt in.
  • When replacing a bufio.Scanner loop with strings.Lines/bytes.Lines, remember the yielded line keeps its trailing \n; add a TrimSuffix if the old code relied on Scanner.Text() semantics.
  • 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.

Automated tooling (Go 1.27+)

Many of these transformations are now shipped as official modernizers in gopls/go fix. On Go 1.27+ the whole set can be applied across a module with:

sh
go fix ./...

gopls v0.22.0 added four notable passes covered above:

ModernizerMin GoTransformation
unsafefuncs1.17uintptr pointer math → unsafe.Add / unsafe.Slice
atomictypes1.19primitive atomic.* funcs → typed atomic.Int32/Pointer[T] wrappers
slicesbackward1.23descending-index loops → slices.Backward iterator
embedlit1.27redundant embedded-field literals → promoted-field init

Other modernizers in the same suite that this catalog covers: minmax, efaceany, fmtappendf, stringscut, stringsseq, sortslice/slicescontains, mapsloop, stditerators, forvar, rangeint, testingcontext, bloop, waitgroup, newexpr, errorsastype, appendclipped (→ slices.Concat), and plusbuild (→ drop obsolete // +build).

To disable an over-eager pass (e.g. slicesbackward rewriting loops that mutate the slice), scope the run with -fixes or exclude that analyzer in your editor's gopls settings.

© smallnest, MIT. Rendered from Markdown: HTML in the file is shown as text, images as links, and headings moved down two levels. Raw file

Files

Just SKILL.md in skills/modern-go of smallnest/goal-workflow.

Open the folder on GitHubat commit b06ab3c

Used in 1 other repository

We found 1 copy of this SKILL.md (exact, near-identical or edited) in other folders. This page covers the copy in smallnest/goal-workflow, which our catalogue first saw on October 7, 2026.

Compare with similar skills

Modern Go next to the 5 skills that share the most tags, products or categories with it. Stars are the repository's; “used in” counts other GitHub owners with a copy.

Modern Go compared with similar skills
SkillStarsUsed inTokensAuto-checkLicenceRepo updated
Modern Go this skillsmallnest/goal-workflow290—~7.2kAutomated safety check: PassMIT
Releasing MarchatCod-e-Codes/marchat137—~801Automated safety check: PassMIT
Temporal Developerlatitude-dev/latitude-llm4.7k—~1.5kAutomated safety check: PassMIT
Install Mimi Remotegaixianggeng/mimi-remote104—~2.8kAutomated safety check: PassGPL-3.0
Add Redis Command to go-redisredis/go-redis22k—~1.4kAutomated safety check: PassBSD-2-Clause
Go Pedantrychromedp/chromedp13k—~3.7kAutomated safety check: PassMIT

Similar skills

  • Releasing Marchat

    Cod-e-Codes/marchat

    Prepares marchat releases: version bumps, CHANGELOG, packaging checksums, GitHub Actions release workflow, and Docker tags.

    137 GitHub stars~801 tokensUpdated 7 days ago
    DevOps & CloudAuto-check passed
  • Temporal Developer

    latitude-dev/latitude-llm

    This skill should be used when the user asks to "create a Temporal workflow", "write a Temporal activity", "debug stuck workflow", "fix non-determinism error", "Temporal Python", "Temporal…

    4.7k GitHub stars~1.5k tokensUpdated yesterday
    Backend & APIsAuto-check passed
  • Install Mimi Remote

    gaixianggeng/mimi-remote

    安装、配置、配对、迁移、升级、诊断、回滚或卸载 Mimi Remote;在 macOS 上安装和维护 Mimi Remote Mac 菜单栏 App / DMG,或通过 Homebrew、Linux user-systemd 部署 agentd;从源码构建 iPhone/iPad App;配置 Codex 主通道和可选 Claude Code 实验 Runtime。用户提出“安装 Mimi…

    104 GitHub stars~2.8k tokensUpdated yesterday
    MobileAuto-check passed
  • Official

    Walks through adding a new Redis or Redis module command to the go-redis client, from fetching the spec and docs to the type, interface wiring, parsing and tests.

    22k GitHub stars~1.4k tokensUpdated yesterday
    DevelopmentAuto-check passed
  • Go Pedantry

    chromedp/chromedp

    This skill should be used when the user is writing Go code and needs guidance on Go-specific pedantry: error wrapping with fmt.Errorf and %w, interface design (accept interfaces return structs)…

    13k GitHub stars~3.7k tokensUpdated 4 days ago
    DevelopmentAuto-check passed
  • Official

    Audits and updates os-packages.json files listing the Linux packages each .NET release needs per distro, then regenerates the Markdown from the JSON.

    22k GitHub stars~2.3k tokensUpdated yesterday
    DevelopmentAuto-check passed

More from smallnest/goal-workflow

All 20 skills in this repo
  • Article Icons

    smallnest/goal-workflow

    Illustrate an article (Markdown, HTML, etc.) with animated-style icons from itshover.com/icons.

    290 GitHub stars~1.6k tokensUpdated 26 days ago
    Auto-check passed
  • Graph

    smallnest/goal-workflow

    Graph engineering for parallel task execution: convert a task, PRD, SPEC, or issue set into a dependency graph (DAG), layer it into supersteps, then implement each independent node concurrently with…

    290 GitHub stars~3.9k tokensUpdated 26 days ago
    Auto-check passed
  • Walkthrough

    smallnest/goal-workflow

    Generate a Phase-2 Walkthrough artifact (walkthrough.md) once implementation and verification are complete.

    290 GitHub stars~4.7k tokensUpdated 26 days ago
    Auto-check: notes
  • Insight Diagram

    smallnest/goal-workflow

    为任意项目生成 UML 图、架构图和流程图。分析代码库后让用户选择要生成的图表类型,使用 architecture-diagram skill 渲染为 HTML+SVG,保存到 docs/ 目录。适用于任何软件项目的文档可视化。

    290 GitHub stars~1.6k tokensUpdated 26 days ago
    Auto-check passed
  • Code To Spec

    smallnest/goal-workflow

    Reverse-engineer a SPEC document from an existing project. An agent skill from smallnest/goal-workflow.

    290 GitHub stars~2.7k tokensUpdated 26 days ago
    Auto-check passed
  • Design It

    smallnest/goal-workflow

    A skill your agent uses when turning a requirement, spec, or feature brief into a single self-contained HTML design document in a fixed house style — one styled HTML page with a table-of-contents…

    290 GitHub stars~1.1k tokensUpdated 26 days ago
    Auto-check passed

Works with

Categories

Questions about Modern Go

What does Modern Go do?

Modernize Go code by applying version-appropriate idioms and APIs (gofix-style transformations). Modern Go is an agent skill from smallnest/goal-workflow. Modernize Go code by applying version-appropriate idioms and APIs (gofix-style transformations).

When should I use Modern Go?

Modern Go fits situations like: the user says 现代化; wants to upgrade Go idioms.

How do I install Modern Go in Claude Code?

Run `npx skills add smallnest/goal-workflow --skill modern-go -a claude-code`. Or copy the skill folder (skills/modern-go in smallnest/goal-workflow) into .claude/skills/modern-go in your project. Claude Code loads it when a task matches its description.

How do I install Modern Go in Codex?

Run `npx skills add smallnest/goal-workflow --skill modern-go -a codex`. Or copy the skill folder (skills/modern-go in smallnest/goal-workflow) into .agents/skills/modern-go in your project. Codex loads it when a task matches its description.

Can I use Modern Go in Cursor, Gemini CLI or GitHub Copilot?

Cursor, Gemini CLI, GitHub Copilot and OpenCode also load SKILL.md folders. With the skills CLI, run `npx skills add smallnest/goal-workflow --skill modern-go -a cursor` (or -a gemini-cli, github-copilot or opencode for the others). To copy it by hand, put the folder in .cursor/skills/modern-go, .gemini/skills/modern-go, .github/skills/modern-go and .opencode/skills/modern-go in your project.

What does Modern Go need to run?

Going by SKILL.md and its folder, Modern Go needs the command-line tools its instructions call (go).

Does Modern Go access the network?

SKILL.md contains no URLs. Any network use would come from the scripts or tools the agent runs. This is read from the text; nothing was executed.

Is Modern Go safe to install?

Our automated static check of SKILL.md found no risky patterns, such as piping downloads into a shell, reading credential files or hidden Unicode. It is not a guarantee. Review the folder before installing.

What licence does Modern Go use?

Modern Go is published under the MIT licence (the repository's licence). It allows redistribution, so the full SKILL.md is shown on this page.

How many tokens does Modern Go use?

About 7.2k tokens (SKILL.md is roughly 29k characters). Agents keep only the skill's name and description in context until a task matches; then they load SKILL.md in full.

What are the alternatives to Modern Go?

Skills that share tags, products or a category with Modern Go: Releasing Marchat (Cod-e-Codes/marchat, 137 stars), Temporal Developer (latitude-dev/latitude-llm, 4.7k stars), Install Mimi Remote (gaixianggeng/mimi-remote, 104 stars) and Add Redis Command to go-redis (redis/go-redis, 22k stars). The comparison table on this page puts their stars, adoption, token cost, safety result and licence side by side.

Who maintains Modern Go?

smallnest (a GitHub user) maintains it in smallnest/goal-workflow, which has 290 GitHub stars. The repository holds 20 skills in this directory. The repository was last updated on September 13, 2026.

Source: smallnest/goal-workflow on GitHub. Facts on this page come from the repository at the commit we read; the author's words are quoted as theirs.