# Go Idioms

> Modern idiomatic Go (`modernize`) — Go 1.26+, 1.27 additions noted. This skill should be used when a diff contains a rewritable construct, when the user asks to modernize Go or run `go fix`, or asks which fixer owns a rewrite — range-over-int, `min`/`max`, `slices`/`maps`, `strings.Cut`, `any`, iterators, `omitzero`, `os.Root`, `new(expr)`, `errors.AsType`, and Go 1.27's `atomictypes`, `embedlit`, `slicesbackward`, `unsafefuncs` — rewrites go to `go fix ./...` or `golangci-lint --enable-only=modernize`, a nested `:=` that shadows `err` to the opt-in `shadow` analyzer, a redundant `break` in a `switch` to staticcheck S1023. Not for linter configuration (go-lint-setup). Go only.

- Skill: `cadasto/go-idioms` (Agent Skill)
- Install (CLI): `npx skillmds@latest add cadasto/go-idioms`
- Raw SKILL.md: https://api.skillmd.com/api/skills/cadasto/go-idioms/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: Cadasto (https://skillmd.com/u/cadasto)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/cadasto/go-idioms

---


# go-idioms — modern Go (modernize)

**Advice == tooling.** The `modernize` analyzers flag and usually auto-fix most of what follows — the
**Fixer** column says which, and the last section covers what no fixer automates. Run the tool,
don't hand-audit. As of **Go 1.26** the rewritten `go fix` is the canonical runner — it ships the
modernizer suite in the toolchain itself:

```
go fix -diff ./...                                  # preview the rewrite as a unified diff (clean tree first)
go fix ./...                                        # apply; -<fixer> runs one, -<fixer>=false excludes one
golangci-lint run --enable-only=modernize --fix     # the x/tools modernize suite — includes the † fixers below
```

Both draw on the same `golang.org/x/tools` engine as gopls, but golangci-lint pins its own (usually
newer) snapshot of it — that gap is what the **†** marker below tracks. This skill explains *why*
and catches what review notices before the tool runs. The **baseline is Go 1.26.4+** (Go 1.27 is
supported too; its additions are flagged as hints in **Newer in Go 1.27** below), so every row
below applies as written — the `Since` column is provenance: it explains why older code looks
different, and what an older module would have to bump to before adopting the idiom.

## Prefer → over (since)

The **Fixer** column names the analyzer that owns each rewrite. Plain = registered in the Go 1.26.4
toolchain's `go fix` (ground truth: `go tool fix help`; per-fixer docs: `go tool fix help <name>`).
**†** = only in the newer `x/tools` suite so far — golangci-lint's `modernize` and gopls run it, the
1.26.4 toolchain's `go fix` does not. `—` = no fixer exists: review has to catch it. Two † rows
below (`atomictypes`, `slicesbackward`) graduate into the stock `go fix` on 1.27, which also renames
`waitgroup` → `waitgroupgo` and drops `fmtappendf` (<https://go.dev/doc/go1.27>) — see
**Newer in Go 1.27** below rather than reading that as a change to this table.

| Prefer | Over | Since | Fixer |
|---|---|---|---|
| `new(expr)` — e.g. `Field: new(30)`, `new(int64(req.Limit))` | a `ptr[T](v)` helper or a hand-written `tmp := v; &tmp`, for optional/pointer fields | 1.26 | `newexpr` |
| `errors.AsType[E](err)` | `errors.As(err, &target)` → `go-errors` | 1.26 | `errorsastype` † |
| `for i := range n` | `for i := 0; i < n; i++` | 1.22 | `rangeint` |
| `min(a, b)` / `max(a, b)` builtins | hand-rolled helpers | 1.21 | `minmax` |
| *(drop)* `x := x` loop-var copy | pre-1.22 capture workaround | 1.22 | `forvar` |
| `any` | `interface{}` | 1.18 | `any` |
| `slices.Sort/Contains`, `slices.Collect`, `maps.Keys` | hand-rolled sort/contains/map loops | 1.21–1.23 | `slicescontains`, `slicessort`, `mapsloop` |
| `for i, v := range slices.Backward(s)` | `for i := len(s)-1; i >= 0; i--` | 1.23 | `slicesbackward` † |
| `strings.Cut` / `CutPrefix` / `CutSuffix` | `Index` + manual slicing | 1.18/1.20 | `stringscut`, `stringscutprefix` |
| `strings.SplitSeq` / `FieldsSeq` | ranging over `strings.Split`/`Fields` (allocates a slice) | 1.24 | `stringsseq` |
| `fmt.Appendf(b, …)` | `append(b, fmt.Sprintf(…)...)` / `[]byte(fmt.Sprintf(…))` | 1.19 | `fmtappendf` |
| `omitzero` on a struct-typed json field | `omitempty`, which does **nothing** for struct fields — a zero `time.Time` still marshals | 1.24 | `omitzero` |
| `t.Context()` in tests | `context.WithCancel(context.Background())` → `go-testing` | 1.24 | `testingcontext` |
| `reflect.TypeFor[T]()` | `reflect.TypeOf((*T)(nil)).Elem()` | 1.22 | `reflecttypefor` |
| `cmp.Or(a, b, …)` | nested `if x == "" { x = y }` | 1.22 | — |
| `sync.OnceFunc` / `OnceValue` | `sync.Once` + a captured var | 1.21 | — |
| `iter.Seq[V]` / range-over-func | `Visit(callback)` patterns, exposing slices | 1.23 | `stditerators` |
| `slog.LogAttrs(ctx, lvl, msg, attrs…)` on hot paths | key-value variadic `slog` (allocates) | 1.21 | — |
| `errors.Join` | manual multi-error concat → `go-errors` | 1.20 | — |
| `wg.Go(...)` | `wg.Add(1)`/`defer wg.Done()` → `go-concurrency` | 1.25 | `waitgroup` |
| `for b.Loop()` | `for i := 0; i < b.N; i++` → `go-testing` | 1.24 | `bloop` † |
| typed `atomic.Int64` | bare-int `atomic.Add*` → `go-concurrency` | 1.19 | `atomictypes` † |

Idioms are a moving target — let the tool (pinned to the repo's toolchain) be the source of
truth so advice never drifts from the user's `go fix`. Go 1.26 also lifts the ban on a generic type
referencing itself in its own type-parameter list (e.g. `type Adder[A Adder[A]] interface{ Add(A) A }`),
so self-referential constraints no longer need a workaround — but that's a hand-written pattern, not
something a modernizer rewrites.

## Modern, but no fixer automates it

- **`os.OpenRoot(dir)` → `*os.Root`** (1.24) for anything that opens a caller-supplied path: its
  methods cannot escape the directory, including via symlink. Replaces `filepath.Join` plus
  hand-written `..`/prefix checks — the traversal-bug pattern those checks keep getting wrong.
- **`rand.Text()`** from `crypto/rand` (1.24) for tokens, nonces, and IDs — not `math/rand`, and not
  a hand-rolled base64 of `rand.Read`. Security-sensitive randomness always comes from `crypto/rand`.
- **`var s []T`, not `s := []T{}`** — the nil slice is the idiomatic empty slice (append works,
  `len` is 0). Reach for the non-nil literal only when something genuinely distinguishes them
  (e.g. marshalling `[]` vs `null`).
- **`slices.Sorted(maps.Keys(m))`** (1.23) when iterating a map for output — map order is random, and
  unstable output is a flaky-test and noisy-diff source.
- **A nested `:=` can shadow `err` or `ctx`.** `if v, err := f(); err != nil { … }` inside a function
  that already has an `err` declares a second one; the outer stays nil and a later `return err`
  reports success. Use `=` to assign into the existing variable, or give the inner one another name.
  Nothing in the standard set reports this; the `shadow` analyzer from `golang.org/x/tools` does
  when enabled (in golangci-lint: `linters.settings.govet.enable: [shadow]`) — it is noisy on
  legitimate reuse, so a repo enables it deliberately rather than by default.
- **No `break` at the end of a `switch` case.** Go cases do not fall through, so a `break` as the
  last statement of a case is dead text; `staticcheck` S1023 (standard set) flags exactly that.
  Earlier in a case a `break` still does work — it leaves the `switch` from that point — and a
  labelled `break` leaves the enclosing loop instead; neither of those is dead text.

*Go 1.27 (released 2026-08-19, <https://go.dev/dl/>) graduates several † fixers into the toolchain's
`go fix` (`atomictypes`, `slicesbackward`, plus new `embedlit` and `unsafefuncs`), renames `waitgroup`
→ `waitgroupgo`, and drops `fmtappendf`; it also lands `encoding/json/v2` + `encoding/json/jsontext`
(v1 is reimplemented on v2, opt out with `GOEXPERIMENT=nojsonv2`) and `strings.CutLast`/`bytes.CutLast`.
Source: <https://go.dev/doc/go1.27>. See **Newer in Go 1.27** below for the hints these enable — none
of it is required on the 1.26 floor.*

## Newer in Go 1.27 (hints, not requirements)

Go 1.27 is additive over 1.26 — every 1.26 rule above still applies unchanged, and nothing here is
required while a module's `go` directive stays at 1.26. Once a repo's toolchain (and `go` directive)
moves to 1.27, these are worth reaching for.

| Idiom (available from 1.27) | Supersedes / complements | Fixer / linter | Since | Source |
|---|---|---|---|---|
| Generic methods — a method may declare its own type parameters (interface methods still may not declare type parameters, nor be implemented by generic methods) | a package-level generic helper function bound to the receiver type as a workaround for "methods can't be generic" | — | 1.27 | [go.dev/doc/go1.27](https://go.dev/doc/go1.27) |
| `encoding/json/v2` + `jsontext` when you want v2's stricter semantics (invalid UTF-8 and duplicate object names rejected) or its `Options` | v1 `encoding/json` — **keep it**: it now runs on the v2 implementation underneath, keeps its behaviour (only exact error text may shift), gets the faster unmarshal for free, and stays supported. The release notes are explicit: "users are not required to migrate". Opt out of the new backend with `GOEXPERIMENT=nojsonv2` | — | 1.27 | [go.dev/doc/go1.27](https://go.dev/doc/go1.27) |
| `atomictypes` — graduates into the stock `go fix` (previously † golangci-lint-only, row above) | raw `sync/atomic` functions | `atomictypes` | 1.27 | [go.dev/doc/go1.27](https://go.dev/doc/go1.27) |
| `slicesbackward` — graduates into the stock `go fix` (previously † golangci-lint-only, row above) | `for i := len(s)-1; i >= 0; i--` | `slicesbackward` | 1.27 | [go.dev/doc/go1.27](https://go.dev/doc/go1.27) |
| `embedlit` — initialise a field promoted from an embedded struct directly in the parent literal: `T{U: U{x: 1}}` → `T{x: 1}` | the nested literal an embedded struct used to require | `embedlit` | 1.27 | [modernize](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize) |
| `unsafefuncs` — `unsafe.Pointer(uintptr(ptr) + uintptr(n))` → `unsafe.Add(ptr, n)` | hand-rolled unsafe pointer arithmetic (`unsafe.Add` itself is 1.17; the fixer is new) | `unsafefuncs` | 1.27 | [modernize](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize) |

- **`go test` runs the `stdversion` vet check by default from 1.27.** The check itself is not new —
  what changes is that it becomes automatic. It flags stdlib symbols newer than the `go` directive in
  force for the file, which is the guardrail that keeps a 1.26-floor module from silently depending on
  a 1.27-only symbol. Trust it over manual review for this. Source:
  [go.dev/doc/go1.27](https://go.dev/doc/go1.27).

## Sources
- `modernize` (per-fixer docs, the Fixer column) — <https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize>
- `go fix` (rewritten in 1.26) — <https://go.dev/blog/gofix>; range-over-func — <https://go.dev/blog/range-functions>
- `slog` — <https://go.dev/blog/slog>; Go 1.21–1.26 release notes (`new(expr)`, self-ref generics — <https://go.dev/doc/go1.26>)
- `os.Root` / `omitzero` / `rand.Text` — <https://go.dev/doc/go1.24>; Code Review Comments (Declaring Empty Slices, Crypto Rand) — <https://go.dev/wiki/CodeReviewComments>
- Go 1.27 release notes (generic methods, `stdversion`, `encoding/json/v2`, new `go fix` modernizers) — <https://go.dev/doc/go1.27>
- Google Go Style Decisions (Switch and break, Nil slices) — <https://google.github.io/styleguide/go/decisions>; Best Practices (Shadowing) — <https://google.github.io/styleguide/go/best-practices>; staticcheck S1023 — <https://staticcheck.dev/docs/checks/#S1023>; `shadow` analyzer — <https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/shadow>
- `atomictypes` / `slicesbackward` / `embedlit` modernizer commits — <https://github.com/golang/tools/commit/17ee9acf0e54b52b93b8250245ea261f5e4a88ec>, <https://github.com/golang/tools/commit/b96d2a55a08943af1de2914a59fb88fe0acbb897>, <https://github.com/golang/tools/commit/c2a9c879aa8aea10399b942692b75107790bbcd7>

---
*Decomposition inspired by [`samber/cc-skills-golang`](https://github.com/samber/cc-skills-golang) (MIT © 2026 Samuel Berthe); rules grounded in the sources above.*

