| name | go-version-feature-map |
| description | The consolidated Go 1.21→1.26 feature reference and modernization rule — which idiom is available given the module's `go` directive, and prefer the modern builtin/stdlib form over the stale hand-rolled one. The `go` line gates which language features compile (range-over-func needs `go 1.23`, per-iteration loop variables need `go 1.22`), so "idiomatic" means *current* idiomatic for the declared version. Catches the pre-modern Go that older training data emits: the `tc := tc` loop copy (unneeded since 1.22), `for i := 0; i < b.N; i++` benchmarks (use `b.Loop`, 1.24), hand-rolled `min`/`max`/`Contains` (builtins/`slices`, 1.21), `omitempty` on `time.Time` (use `omitzero`, 1.24), the `errors.As` out-param dance (use `errors.AsType[T]`, 1.26), and the `tools.go` hack (use `tool` directives, 1.24). Auto-invokes when choosing a Go idiom that depends on version, setting or raising the `go` directive, modernizing old Go, or on "what version is this from" / "is there a newer way" questions. |
| allowed-tools | Read, Glob, Grep |
| compatibility | Claude Code, Codex CLI, Gemini CLI |
Go Version Feature Map
"The go line for each module sets the language version the compiler enforces when compiling packages in that module."
— Go Toolchains
"the compiler rejects use of language features introduced after the version specified by the go directive"
— Go Modules Reference
Go evolves fast, and most LLM training data is older than the toolchain in front of you. The recurring failure is pre-modern Go: code that compiles but is written for a Go that predates the builtins, loop semantics, and stdlib helpers the current release ships. This skill is the consolidated version reference (1.21→1.26) and one rule: check the module's go directive, then use the modern form it unlocks. "Idiomatic" is not timeless — it means current-idiomatic for the version you declared.
This skill is the table; the feature-owning skills (go-error-handling, go-slices-and-maps, go-json, …) own the depth. The go directive itself — how to set and raise it — is owned by go-modules-and-versioning.
1. The go Directive Gates Which Features Compile
The go line in go.mod is not cosmetic metadata. It "sets the language version the compiler enforces" (Go Toolchains), and "the compiler rejects use of language features introduced after the version specified by the go directive" (Modules Reference). So:
go 1.21 → min/max/clear builtins available; range-over-func and per-iteration loop variables are not.
go 1.22 → per-iteration loop variables and for i := range n turn on.
go 1.23 → range-over-func iterators compile.
Two consequences that this skill exists to keep straight:
- Down-gating breaks modern code. Writing
for range seq (a range-over-func) in a module that declares go 1.21 is a compile error, not a runtime surprise. If you need a feature, the go line must be at least its version.
- Some features are gated by the directive even when the toolchain is newer. The 1.22 loop-variable change and the 1.23 timer-channel change are tied to the module's
go line, not the installed compiler — Go 1.26 still compiles a go 1.21 module with the old loop semantics (go1.23 — Timer changes: "only enabled when the main Go program is in a module with a go.mod go line using Go 1.23.0 or later"). Per-file build constraints can raise the language version for one file (Go Toolchains).
Raising the go line is a real decision — it unlocks idioms and locks out consumers on older Go. Before bumping it, see go-modules-and-versioning §4 for the directive/toolchain mechanics; this skill tells you what each line unlocks.
The operational check (with this skill's Read/Grep tools): before choosing or modernizing an idiom, find the gate first.
grep -E '^go [0-9]' go.mod
Then consult §3 / references/version-table.md: if the idiom's minimum version is <= the go line, use the modern form; if it is higher, either use the older form or (deliberately) raise the line. Never emit a feature newer than the declared version — it will not compile.
2. The Modernization Rule
Prefer the modern builtin, the modern stdlib helper, and the modern language form — up to the limit the go directive allows. Recognize the stale pre-modern pattern and replace it.
When you author or review Go, run the value/loop/error/test construct against the version map:
- A hand-rolled
func max(a, b int) int is stale on go 1.21+ — max is a builtin.
- A
tc := tc copy inside a for _, tc := range cases is stale on go 1.22+ — the loop variable is already per-iteration.
- A
for i := 0; i < b.N; i++ benchmark is stale on go 1.24+ — use for b.Loop().
omitempty on a time.Time field is a bug on go 1.24+ — it never omits a zero time; use omitzero.
- The
errors.As(err, &target) out-parameter dance is stale on go 1.26+ — use errors.AsType[T](err).
The full mapping — feature, the OLD pattern it replaces, the idiomatic NEW form, and a primary-source URL — is in ${CLAUDE_SKILL_DIR}/references/version-table.md. The high-frequency stale-pattern fixes are in ${CLAUDE_SKILL_DIR}/references/common-mistakes.md.
3. The Version Map at a Glance
Each row: the headline additions and the stale pattern they retire. → reads "replaces." Full detail and per-feature URLs in references/version-table.md.
go line | Headline language/builtin additions | Headline stdlib additions | Retires (stale → modern) |
|---|
| 1.21 | min, max, clear builtins | slices, maps, cmp; log/slog; sync.OnceFunc/OnceValue/OnceValues; context.WithoutCancel/AfterFunc/WithDeadlineCause | hand-rolled max/min/Contains/Sort → builtins + slices; log/fmt.Println → slog |
| 1.22 | per-iteration loop variables; for i := range n (range over int) | math/rand/v2; routing patterns in net/http.ServeMux | x := x capture copy → (nothing); for i := 0; i < n; i++ → for i := range n |
| 1.23 | range-over-func iterators (for range seq) | iter; slices/maps iterator funcs (slices.Sorted, maps.Keys, …); unique; GC-able/synchronous timer channels | manual key-collection loops → slices.Sorted(maps.Keys(m)); leaked unstopped timers |
| 1.24 | generic type aliases | omitzero JSON tag; tool directives; testing.B.Loop; weak; runtime.AddCleanup; os.Root; Swiss-table maps | omitempty on time.Time → omitzero; tools.go hack → tool; for i := 0; i < b.N → b.Loop(); runtime.SetFinalizer → AddCleanup |
| 1.25 | (no language changes) | testing/synctest (stable); sync.WaitGroup.Go; encoding/json/v2 (experiment); container-aware GOMAXPROCS; go vet waitgroup/hostport; trace flight recorder; Green Tea GC (experiment) | manual wg.Add(1)/go/defer wg.Done() → wg.Go(...); flaky time-based concurrency tests → synctest |
| 1.26 | new(expr) (initializer); self-referential generic constraints | errors.AsType[T]; slog.NewMultiHandler; Green Tea GC (default); go fix modernizers + //go:fix | errors.As(err, &t) out-param → errors.AsType[T](err); p := &x boxing dance → new(expr) |
4. Per-Version Highlights (stale → modern)
The features most likely to catch an LLM emitting old Go. Each shows the pattern it retires.
1.21 — min/max/clear are builtins. "The new functions min and max compute the smallest (or largest, for max) value of a fixed number of given arguments"; "The new function clear deletes all elements from a map or zeroes all elements of a slice" (go1.21). Stop writing a generic Max[T] helper or a for k := range m { delete(m, k) } loop. The slices/maps/cmp packages and log/slog also promote to the stdlib here.
func maxInt(a, b int) int { if a > b { return a }; return b }
hi := max(a, b)
1.22 — the loop variable is per-iteration. "Previously, the variables declared by a 'for' loop were created once and updated by each iteration. In Go 1.22, each iteration of the loop creates new variables, to avoid accidental sharing bugs" (go1.22). The classic x := x copy before a goroutine or t.Parallel() is dead weight on go 1.22+. This is gated on the go line: a go 1.21 module still has the old, buggy semantics. Also: "'For' loops may now range over integers" — for i := range 10 (go1.22).
for _, v := range xs { v := v; go use(v) }
for _, v := range xs { go use(v) }
for i := 0; i < n; i++ { ... }
1.23 — range-over-func iterators. The for-range clause "now accepts iterator functions" of type func(func() bool), func(func(K) bool), and func(func(K, V) bool) (go1.23). The iter package and iterator-returning helpers (slices.Sorted, slices.All, maps.Keys, maps.Values) land here. Owned by go-iterators-rangefunc.
keys := make([]string, 0, len(m))
for k := range m { keys = append(keys, k) }
sort.Strings(keys)
for _, k := range keys { use(k) }
for _, k := range slices.Sorted(maps.Keys(m)) { use(k) }
1.24 — omitzero, tool, b.Loop. "a struct field with the new omitzero option ... will be omitted if its value is zero ... unlike omitempty, omitzero omits zero-valued time.Time values, which is a common source of friction" (go1.24). Benchmarks "may now use the faster and less error-prone testing.B.Loop method ... in place of the typical loop structures involving b.N" (go1.24). And tool directives "remove the need for the previous workaround of adding tools as blank imports to a file conventionally named 'tools.go'" (go1.24).
At time.Time `json:"at,omitempty"`
At time.Time `json:"at,omitzero"`
for i := 0; i < b.N; i++ { ... }
1.25 — WaitGroup.Go, synctest. "The new WaitGroup.Go method makes the common pattern of creating and counting goroutines more convenient" (go1.25) — it does the Add/Done for you. testing/synctest "graduated to general availability" for deterministic, virtual-time concurrency tests (go1.25). encoding/json/v2, container-aware GOMAXPROCS, and Green Tea GC arrive as opt-ins this release.
wg.Add(1); go func() { defer wg.Done(); work() }()
wg.Go(func() { work() })
1.26 — errors.AsType[T], new(expr). "The new AsType function is a generic version of As. It is type-safe, faster, and, in most cases, easier to use" (go1.26). "The built-in new function ... now allows its operand to be an expression, specifying the initial value" (go1.26). Self-referential generic constraints (type Adder[A Adder[A]] interface{...}) also become legal.
var nf *NotFoundError; if errors.As(err, &nf) { ... }
if nf, ok := errors.AsType[*NotFoundError](err); ok { ... }
age := years(t); p := &age
5. The Modernization Tool: go fix (1.26)
You do not have to apply these rewrites by hand. As of Go 1.26, "The venerable go fix command has been completely revamped and is now the home of Go's modernizers. It provides a dependable, push-button way to update Go code bases to the latest idioms and core library APIs" — including "a source-level inliner that allows users to automate their own API migrations using //go:fix inline directives" (go1.26). The same fixers ship in the modernize analyzer (golang.org/x/tools/.../modernize, runnable via gopls). Run them and the loop-copy, hand-rolled min, and b.N patterns get rewritten for you. The tool/CI wiring is owned by go-tooling-and-static-analysis; this skill is the human-readable table of what they modernize to.
6. The Failure This Skill Targets
The recurring LLM failure is emitting pre-modern Go because the training data is older than the toolchain. The tells, all corrected by the version map:
tc := tc inside a range loop (the copy is unnecessary on go 1.22+).
for i := 0; i < b.N; i++ benchmarks (use for b.Loop() on go 1.24+).
- A hand-rolled
func max(a, b int) int or func contains(...) (max is a builtin since 1.21; slices.Contains exists).
interface{} where any reads (the any alias has existed since 1.18).
omitempty on a time.Time (it never drops a zero time; use omitzero on 1.24+).
errors.As(err, &target) when errors.AsType[T](err) is available (1.26).
- A
tools.go blank-import file on a 1.24+ module (use tool directives).
The opposite failure also exists: using a feature newer than the go directive allows. Writing a range-over-func in a go 1.21 module is a compile error. The rule covers both directions — match the idiom to the declared version.
7. Routing to the Feature-Owning Skills
This skill is the map; each feature's depth lives in its owner.
go-idiomatic-discipline — the policy root: "idiomatic" means current idiomatic, which is exactly what this map operationalizes.
go-modules-and-versioning — tight link: owns the go directive and toolchain line mechanics (how to set/raise §1's gate); this skill owns what each line unlocks.
go-tooling-and-static-analysis — owns go fix / the modernize analyzer that auto-applies §4's rewrites (§5).
go-concurrency-goroutines — loop variable (1.22) in goroutines, WaitGroup.Go (1.25).
go-testing-tabledriven / go-testing-advanced — tc := tc removal (1.22), b.Loop (1.24), synctest (1.25).
go-json — omitzero (1.24), encoding/json/v2 (1.25 experiment).
go-error-handling — errors.AsType[T] (1.26), errors.Join (1.20).
go-slices-and-maps — min/max/clear (1.21), the slices/maps packages and their iterator funcs (1.21/1.23).
go-iterators-rangefunc — range-over-func and iter (1.23).
go-generics — generic type aliases (1.24), self-referential constraints (1.26).
8. Reference Files
The full consolidated 1.21→1.26 feature → replacement → version table, with a per-feature primary-source URL:
${CLAUDE_SKILL_DIR}/references/version-table.md
High-frequency stale-Go anti-patterns, each STALE vs MODERN with the version gate and a release-note citation:
${CLAUDE_SKILL_DIR}/references/common-mistakes.md
Source provenance for every claim in this skill:
${CLAUDE_SKILL_DIR}/references/sources.yaml