golang · diff

git:20260823.3e05b04 to git:20260828.71fd2ce

234 added, 588 removed. Audit A to A.

---
name: golang
description: >-
- Go language conventions, idioms, and toolchain. Invoke when task involves any interaction
- with Go code — writing, reviewing, refactoring, debugging, or understanding Go projects.
+ Write and review Go: idioms, error handling, concurrency, testing, project structure, the toolchain, and the language
+ version a module's `go` directive permits.
+ when_to_use: >-
+ Invoke whenever Go code is touched at all — writing, reviewing, refactoring, debugging, or exploring a Go project, or
+ raising a module's `go` directive. Also invoke on the symptoms: a goroutine leaks or races, an error loses its
+ context, a test flakes on timing, `go test` rejects a standard-library symbol, or a package resists naming. Covers
+ the language, its standard library, and its toolchain; templ templates, Zog schemas, and Charm TUIs have their own
+ skills, and language-agnostic workflow belongs to the coding skill.
+ compatibility: Uses Claude Code frontmatter beyond the Agent Skills spec (when_to_use)
---
- # Go
-
- Simplicity is the highest Go virtue. Resist abstraction until the cost of not abstracting is proven.
-
- ## References
-
- Extended examples, code patterns, and detailed rationale for the rules below live in `${CLAUDE_SKILL_DIR}/references/`.
-
- - **[`${CLAUDE_SKILL_DIR}/references/idioms.md`]** — Naming, declarations, interfaces, receivers, configuration,
- embedding: extended code examples for each idiom, Go/bad vs good comparisons, decision criteria tables
- - **[`${CLAUDE_SKILL_DIR}/references/gotchas.md`]** — Variable shadowing, defer traps, slice mutation, strings, copy
- safety: annotated code showing each pitfall with fix patterns, global state examples
- - **[`${CLAUDE_SKILL_DIR}/references/errors.md`]** — Error creation, wrapping, Is/As, structured errors (golib/e): error
- type decision tree, golib/e API (sentinels, fields, logging), wrapping context examples
- - **[`${CLAUDE_SKILL_DIR}/references/concurrency.md`]** — Goroutines, channels, context, sync, errgroup, data races:
- worker lifecycle patterns, pipeline/fan-out/fan-in code, data race scenarios with fixes
- - **[`${CLAUDE_SKILL_DIR}/references/testing.md`]** — Table tests, subtests, assertions, test doubles, benchmarks: full
- table-test template, testify usage, parallel subtests, httptest/iotest utilities
- - **[`${CLAUDE_SKILL_DIR}/references/structure.md`]** — Project layout, packages, imports, file organization: package
- naming examples, import grouping, backward-incompatible change staged workflow
-
- ## Naming
-
- ### Variables — The Distance Rule
-
- Name length scales with scope distance.
-
- - **Loop index** — single letter: `i`, `j`, `k`
- - **Short function local** — 1-3 chars: `r` (reader), `b` (buffer), `ctx`
- - **Function parameter** — short but clear: `name`, `path`, `opts`
- - **Package-level** — descriptive: `defaultTimeout`, `maxRetries`
- - **Exported** — self-documenting: `ErrNotFound`, `DefaultClient`
-
- ### Receivers
-
- Use 1-2 letter type abbreviation: `c` for `Client`, `s` for `Server`. Never `self`, `this`, `me`. Be consistent across
- all methods of a type.
-
- ### Initialisms
-
- All-caps for known initialisms: `URL`, `HTTP`, `ID`, `API`, `SQL`, `XML`. In mixed identifiers: `userID`, `httpClient`,
- `xmlHTTPRequest`.
-
- ### Packages
-
- - Short, lowercase, singular: `user`, `http`, `auth`
- - Named by what they provide, not what they contain
- - Never `util`, `common`, `misc`, `shared`, `helpers`, `types`
- - Callers use package name as prefix — don't stutter: `widget.New()` not `widget.NewWidget()`
-
- ### Getters and Setters
-
- No `Get` prefix on getters. Setter uses `Set` prefix: `u.Name()` not `u.GetName()`, `u.SetName(n)`.
+ Simplicity is the highest Go virtue. Three biases decide most calls:
- ### Interface Names
+ - **Resist abstraction until the cost of not abstracting is proven.** An interface, a type parameter, or a layer added
+ in anticipation of a second caller is a defect.
+ - **Reach for the standard library first.** A dependency earns its place by doing what the standard library does not.
+ - **The `go` directive decides what may be written**, not the installed toolchain.
- One-method interfaces use method name plus `-er`: `Reader`, `Writer`, `Formatter`, `Stringer`. Honor canonical names —
- if your type has `String() string`, call it `String`, not `ToString`.
+ ## Language Version
- ### Constants
+ The `go` directive in `go.mod` sets the language version a module may use. Read it before writing code and never reach
+ for a feature above it. The `stdversion` analyzer reports a standard-library symbol newer than the directive: Go 1.27
+ and later fail `go test` on it, earlier toolchains report it under `go vet`.
- MixedCaps only — never `ALL_CAPS` or `K` prefix. Name by role, not value. If a constant has no role beyond its value,
- don't define it. `const MaxRetries = 12` (good) vs `const Twelve = 12` (bad).
+ Floor version per feature. Anything older than 1.21 is available in every supported module.
- ### Unexported Globals
+ - **1.21** — `min`, `max`, `clear` builtins; `slices`, `maps`, `cmp`, `log/slog`; `context.WithoutCancel`;
+ `errors.ErrUnsupported`; `sync.OnceValue`
+ - **1.22** — per-iteration loop variables; `for range int`; `math/rand/v2`; method and wildcard patterns in
+ `net/http.ServeMux`
+ - **1.23** — range-over-func iterators; `iter`; iterator functions in `slices` and `maps`; `unique`; unbuffered timer
+ channels
+ - **1.24** — generic type aliases; `os.Root`; `testing.B.Loop`; `t.Context`; `runtime.AddCleanup`; `omitzero` struct
+ tag; `tool` directives; iterator functions in `strings`
+ - **1.25** — `testing/synctest`; `sync.WaitGroup.Go`; container-aware `GOMAXPROCS`
+ - **1.26** — `new(expr)`; self-referential type constraints; `errors.AsType`; `go fix` modernizers
+ - **1.27** — generic methods; field-selector keys in struct literals; `encoding/json/v2`; `uuid`; `strings.CutLast`
- Plain lowercase: `defaultPort`, `maxRetries`. Error values use `err` prefix: `errNotFound`.
+ Read [`${CLAUDE_SKILL_DIR}/references/versions/go1.NN.md`] — one file per version, `go1.21.md` through `go1.27.md` —
+ when writing against a feature near the module's floor, and whenever raising the directive. Each carries what its
+ version added, which behavior changes the `go` directive gates, and the traps it introduced.
- ### Avoid Repetition
+ ## Naming
- - Package name is part of every qualified reference: `widget.New()` not `widget.NewWidget()`
- - Don't encode type in name: `var users int` not `var numUsers int`
- - Strip context obvious from scope: method on `*Project` uses `Name()` not `ProjectName()`
+ - **Never `util`, `common`, `misc`, `shared`, `helpers`, or `types` as a package name.** Name a package for what it
+ provides; one that resists naming is holding unrelated things.
+ - **Do not stutter.** The package name prefixes every qualified reference: `widget.New()`, never `widget.NewWidget()`.
+ - **Go source files are kebab-case**: `user-service.go`, `http-handler.go`.
## Interfaces
- - **Consumer-side.** Define where used, not where implemented. Producers return concrete types.
- - **No premature interfaces.** Wait for a concrete need. Don't define "for mocking."
- - **Accept interfaces, return structs.**
- - **Never pointer-to-interface.** Interfaces are already reference types.
- - **Small interfaces.** Prefer 1-3 methods. `io.Reader` (1 method) is more powerful than any 10-method interface.
- - **Compile-time verification.** `var _ http.Handler = (*Handler)(nil)`.
-
- ## Receivers
-
- ### Pointer vs Value
-
- Use **pointer receiver** when: method mutates receiver, receiver contains `sync.Mutex` or similar, receiver is a large
- struct, or in doubt (default to pointer).
+ - **Define an interface where it is consumed**, never where it is implemented. Producers return concrete types.
+ - **Never define an interface for mocking.** Wait for a second implementation or a real consumer boundary.
+ - **Assert satisfaction at compile time** where the contract matters: `var _ http.Handler = (*Handler)(nil)`.
- Use **value receiver** when: receiver is a small immutable value type (like `time.Time`), receiver is a map/func/chan
- (already reference types), all fields are value types with no mutability needs.
+ ## Generics
- **Never mix** receiver types on a single type.
+ - **Write the concrete version first.** Add type parameters when two or more implementations differ only by type — not
+ in anticipation of a second caller.
+ - **Constrain by what the body uses.** `cmp.Ordered` when it compares, `any` when it only moves values. A constraint
+ listing methods the function never calls is noise.
+ - **Prefer a type parameter to `any` plus a type switch** when callers know the type. It moves the failure to compile
+ time and removes the assertion.
+ - **A function generic in a type it passes through unchanged is a function on `any`.** Drop the parameter.
+ - **Methods may declare their own type parameters** — put a generic operation in the type's namespace when it belongs
+ there rather than the package's. Interface methods cannot be generic, so a generic method never satisfies an
+ interface.
- ### Maps, Funcs, and Channels
+ ## Receivers
- Already reference types. Never use pointers to them: `func process(m map[string]int)` not
- `func process(m *map[string]int)`.
+ - **Never mix pointer and value receivers on one type.** Default to pointer; take a value receiver only for a small
+ immutable value type.
+ - **Never take a pointer to a map, func, or channel** — they are already reference types.
## Context
- - **First parameter.** `func Foo(ctx context.Context, ...)`.
- - **Never store in structs.** Pass through call chains explicitly.
- - **`context.Background()` only at the top level** — in `main()` or test setup.
- - **Never include `context.Context` in option structs** — pass as separate parameter.
+ - **Never store a `context.Context` in a struct or an option struct.** Pass it as the first parameter.
+ - **`context.Background()` only in `main()` and test setup.**
+ - **Never hand a request context to background work** — it is canceled when the response is sent. Use
+ `context.WithoutCancel(r.Context())`.
## Declarations
- ### Variable Style
-
- - `var` for zero values: `var s string`, `var mu sync.Mutex`
- - `:=` for initializations: `s := "hello"`, `n := computeSize()`
- - Top-level: use `var`, omit type if obvious: `var defaultPort = 8080`
- - Specify type when it differs from expression: `var e error = myError{}`
-
- ### Slices
-
- - Nil slices are valid and preferred: `var s []string`
- - Non-nil zero-length only when JSON encoding matters: `s := []string{}` (nil encodes as `null`, empty slice as `[]`)
- - Check empty: `len(s) == 0`, not `s == nil`
- - Pre-allocate when size known: `make([]T, 0, n)`
-
- ### Maps
-
- - `make(map[K]V)` for programmatic population; `make(map[K]V, n)` with capacity hint
- - Literal for fixed content: `map[string]int{"a": 1, "b": 2}`
-
- ### Structs
-
- - Always use field names in literals
- - Omit zero-value fields unless they provide meaningful context
- - Zero-value struct: `var user User`
- - Pointer: `&T{}` over `new(T)`
-
- ### Enums
-
- Start `iota` at 1 to distinguish from zero-value (unless zero-value has meaning):
- `const ( StatusActive Status = iota + 1; ... )`.
-
- ### Named Result Parameters
-
- Use when they disambiguate or document caller obligations. Don't use just to enable naked returns, or when the name
- repeats the type.
-
- ## Functions
-
- - **Synchronous default.** Let callers add concurrency.
- - **Return errors, never exit.** Only `main()` calls `os.Exit`/`log.Fatal`.
- - **`defer` for cleanup.** Always.
- - **Early return on error.** Happy path at minimum indentation.
- - **Accept `io.Reader`, not filenames.** Improves reusability and testability.
- - **Close transient resources.** `defer r.Body.Close()`, `defer rows.Close()`, `defer f.Close()`.
+ - **Nil slices are valid and preferred**: `var s []string`, tested with `len(s) == 0`. Use `[]string{}` only where the
+ JSON encoding must be `[]` rather than `null`. Pre-allocate when the size is known: `make([]T, 0, n)`.
+ - **`&T{}` for a pointer to a struct, `new(expr)` for a pointer to any other value** — `new(expr)` replaces the
+ hand-rolled `ptr(v)` helper.
+ - **Start `iota` at 1** unless the zero value carries meaning: `const ( StatusActive Status = iota + 1; ... )`.
+ - **Name result parameters only where they document a caller obligation**, never to enable a naked return.
## Error Handling
- ### Core Rules
-
- - **Always check errors.** Never discard with `_`.
- - **Handle once.** Log OR return — never both. If logging, degrade gracefully (don't return the error). If returning,
- wrap with context and let the caller decide.
- - **Wrap with context.** Prefer structured errors when the project uses them: `ErrNotFound.Wrap(err)` or
- `e.NewFrom("context", err)`. Standard fallback: `fmt.Errorf("context: %w", err)`. Avoid "failed to" prefix in both.
- - **Error strings**: lowercase, no trailing punctuation. They compose: `"read config: open file: permission denied"`.
- - **Don't panic.** Return errors. Reserve panic for truly irrecoverable states.
- - **Use `errors.Is`/`errors.As`** — never `==` or direct type assertion on wrapped errors.
-
- ### Error Creation
-
- - **No match needed, static message** — `errors.New("not found")`
- - **No match needed, dynamic message** — `fmt.Errorf("file %q missing", name)`
- - **Caller must match, static message** — exported `var ErrNotFound = errors.New(...)`
- - **Caller must match, dynamic message** — custom error type with `Error()` method
-
- ### Sentinel Errors
-
- Naming: exported `ErrXxx`, unexported `errXxx`. Always wrap sentinels before returning so callers use `errors.Is`, not
- `==`.
-
- ### Custom Error Types
-
- Naming: exported `XxxError`, unexported `xxxError`. Implement `Error() string`. Callers match with `errors.As`.
-
- ### Structured Errors (golib/e)
-
- When a project uses a structured error package like golib/e, prefer it consistently over `fmt.Errorf`. See
- `${CLAUDE_SKILL_DIR}/references/errors.md` for API details.
-
- ### Wrapping: %w vs %v
-
- - `%w` wraps (callers can unwrap with `errors.Is`/`errors.As`) — default choice
- - `%v` creates new error with original's text only — use when underlying error is an implementation detail
- - **Wrap** when: caller provided the input that caused the error, or the underlying error is part of your API contract
- - **Don't wrap** when: error source is an implementation detail (wrapping commits you to the underlying dependency)
-
- ### Wrapping Context
-
- - Keep context succinct: `"get user: %w"` not `"failed to get user: %w"`
- - Place `%w` at end of format string so error text mirrors chain structure (newest-to-oldest)
- - Don't repeat information the underlying error already provides
- - Don't annotate if annotation adds no new information — just return `err`
-
- ### In-Band Errors
-
- Don't use sentinel return values (`-1`, `""`, `nil`) to signal failure. Return `(T, error)` or `(T, bool)`.
-
- ### Must Functions
-
- `MustXYZ` panics on error. Legitimate only for package-level initialization and test helpers. Never use in request
- handlers or runtime code paths.
-
- ### Type Assertions
-
- Always use comma-ok form: `s, ok := val.(string)`. Never `s := val.(string)` (panics on wrong type).
-
- ### Defer Errors
-
- Don't silently ignore errors from deferred calls (`f.Close()`, `rows.Close()`, `resp.Body.Close()`). Propagate close
- error if no prior error exists. When intentionally ignoring, use `_ =` to make it explicit.
-
- ### Internal Panic/Recover
-
- Acceptable only when panics never escape package boundaries and a top-level deferred `recover` translates them to
- errors. Rare — see `${CLAUDE_SKILL_DIR}/references/errors.md` for the full pattern.
-
- ### Error Flow
+ - **Handle an error once — log it or return it, never both.** Logging and returning reports one failure at every level
+ of the stack. If you log, degrade and stop; if you return, wrap and let the caller decide.
+ - **Wrap with context the underlying error does not already carry**: `fmt.Errorf("get user: %w", err)`. Lowercase, no
+ trailing punctuation, no "failed to" prefix, `%w` last so the chain reads newest to oldest.
+ - **Use `%v` instead of `%w` when the wrapped error is an implementation detail** — `%w` commits your API to the
+ dependency that produced it.
+ - **Match with `errors.Is` and `errors.As`, never `==` or a bare type assertion.** `errors.AsType[T]` is the generic
+ form and needs no pre-declared target.
+ - **Wrap a sentinel before returning it** so callers cannot match it with `==`.
+ - **`errors.Join` only where every operation must be attempted** — closing a set of resources, validating every field.
+ Never as a substitute for returning at the first failure on a sequential path.
+ - **Never silently drop an error from a deferred call.** Propagate a close error when no prior error exists; write
+ `_ = f.Close()` where the drop is deliberate.
+ - **`MustXxx` panics only in package-level initialization and test helpers**, never in a request path.
+ - **Prefer the project's structured error package** — `ErrNotFound.Wrap(err)`, `e.NewFrom("context", err)` — over
+ `fmt.Errorf` wherever one is in use, and use it for every error in that project.
- Indent errors, keep happy path flat. Early return on error, never nest the happy path in `else` blocks.
+ Read [`${CLAUDE_SKILL_DIR}/references/conventions/errors.md`] when the project uses golib/e, when designing a new error
+ type, or when a package needs internal panic/recover — it carries the golib/e API, the creation decision tree, and the
+ recover-at-the-boundary pattern.
## Gotchas
- ### Variable Shadowing
-
- `:=` in inner blocks (if/for) silently hides outer variables. The `err` variable is commonly shadowed. Use
- `go vet -shadow` or `golangci-lint` to detect. Always verify assignments in inner blocks use `=` (not `:=`) when
- targeting outer-scope variables.
-
- ### Defer Argument Evaluation
-
- `defer` evaluates arguments immediately, not when the deferred function runs. Use closures to capture current values:
- `defer func() { notify(status) }()`.
-
- ### Defer in Loops
-
- `defer` runs when the surrounding function returns, not at end of loop iteration. Extract loop body to a function so
- `defer` fires per iteration.
-
- ### Slice Append Mutation
-
- `append` on a slice with remaining capacity mutates the underlying array. Slices derived from the same array see each
- other's writes. Fix: use full slice expression `s[:len(s):len(s)]` to cap capacity, or explicit `copy`.
-
- ### Strings: Runes vs Bytes
-
- `len(s)` returns byte count, not rune count. Use `range` over string to iterate runes (not `s[i]`). Use
- `utf8.RuneCountInString(s)` for rune count.
-
- ### String Concatenation
-
- Use `strings.Builder` with `Grow` when concatenating in a loop — `+=` is O(n^2). For a few fixed strings, `+` or
- `fmt.Sprintf` is fine.
-
- ### Copy Safety
-
- - Never copy `sync.Mutex` or types containing one
- - Don't copy structs with pointer fields unless you understand aliasing
- - Copy slices/maps at API boundaries to prevent external mutation
-
- ### Fixed Bit-Width Types
-
- Prefer `int` unless a specific width is required by a protocol, binary format, or performance constraint. `int8`,
- `uint16`, etc. are prone to silent overflow.
-
- ### Signal Boosting
-
- When code does the opposite of what's common (checking `err == nil` instead of `err != nil`, a deliberately inverted
- guard), name the condition instead of commenting it: `if cacheHit := err == nil; cacheHit {`. A named condition is read
- at the branch and cannot go stale; a comment saying "this is intentional" can.
-
- ### Typed Nil Interface Trap
+ - **`:=` in an inner block silently shadows the outer variable**, `err` most often. Assign with `=` when the target is
+ an outer variable.
+ - **`defer` evaluates its arguments immediately.** Capture later values in a closure: `defer func() { notify(s) }()`.
+ - **`defer` fires at function return, not at the end of a loop iteration.** Extract the loop body into a function.
+ - **`append` to a slice with spare capacity mutates the shared backing array**, and every slice over that array sees it.
+ Cap with `s[:len(s):len(s)]` or copy.
+ - **A nil pointer in an interface is not nil.** Return an explicit `nil` from a function whose result type is an
+ interface, never a typed nil.
+ - **Never copy a `sync.Mutex` or a struct holding one.** Copy slices and maps at API boundaries so callers cannot mutate
+ your state.
+ - **Prefer `int`.** Sized integer types overflow silently, and only a protocol, a binary format, or a measured
+ constraint justifies one.
+ - **`+=` in a loop is O(n²).** Use `strings.Builder` with `Grow`.
+ - **Name an inverted condition instead of commenting it**: `if cacheHit := err == nil; cacheHit {`. A name is read at
+ the branch and cannot go stale.
- A `(*T)(nil)` assigned to an interface is non-nil. Return explicit `nil` when the function returns an interface type,
- never a typed nil.
+ Read [`${CLAUDE_SKILL_DIR}/references/conventions/gotchas.md`] when one of these bites and the obvious fix does not hold
+ — it carries an annotated reproduction and fix for each.
## Concurrency
- ### Goroutine Lifecycle
-
- Every goroutine must have: (1) a predictable exit condition, and (2) a way for other code to wait for it to finish. No
- fire-and-forget goroutines — they leak memory and cause data races.
-
- ### Cancellation: context.Context (Primary)
-
- Use `context.Context` as the default for all goroutine lifecycle management. Caller creates context with cancel,
- goroutine selects on `ctx.Done()`.
-
- ### Joining: sync.WaitGroup
-
- Use `sync.WaitGroup` to wait for multiple goroutines to finish. It handles joining, not cancellation. Call `wg.Add(1)`
- before launching, `defer wg.Done()` inside the goroutine.
-
- ### Worker Lifecycle Pattern
-
- For long-lived goroutines, wrap in a struct with `context.Context` for cancellation and a done channel or `WaitGroup`
- for joining. Close done channel via `defer close(w.done)` in the run method.
-
- ### Stop + Done Channels (Alternative)
-
- When `context.Context` is unavailable (infrastructure code predating context), use explicit stop/done channels. Prefer
- `context.Context` in new code.
-
- ### Channels vs Mutexes
-
- - **Parallel goroutines accessing shared state** — `sync.Mutex` (synchronization)
- - **Concurrent goroutines coordinating work** — channels (communication/orchestration)
- - **Transferring ownership of a resource** — channels (signaling completion)
-
- Mutexes protect shared state. Channels coordinate independent actors.
-
- ### Channel Rules
-
- - **Size: zero or one.** Larger buffers require justification — you must know what prevents the channel from filling.
- - **Direction in signatures.** Specify `<-chan` or `chan<-` in function parameters/returns.
- - **Close from sender side.** Never close from receiver.
- - **Sends on closed channels panic.** Ensure all sends finish before closing.
-
- ### Pipeline Pattern
-
- Stages connected by channels: each stage receives from upstream, processes, sends downstream. Close output channel via
- `defer close(out)` in the goroutine.
-
- ### Fan-Out, Fan-In
-
- Fan-out: multiple goroutines read from one channel. Fan-in: merge multiple channels into one using a WaitGroup to close
- the merged channel when all inputs are done.
-
- ### Bounded Parallelism
-
- Limit concurrent work with a fixed worker pool reading from a shared channel.
-
- ### Select Behavior
-
- When multiple cases are ready, `select` picks one at random — not in source order. For priority, drain the work channel
- after receiving the stop signal.
-
- ### Nil Channels
-
- A nil channel blocks forever on send and receive. Set a channel to `nil` to remove it from a `select` at runtime.
-
- ### errgroup
-
- Prefer `errgroup.WithContext` over manual `sync.WaitGroup` + error collection. It manages goroutine groups with error
- propagation and context cancellation. First non-nil error from any goroutine is returned by `g.Wait()`.
-
- ### Context Propagation
-
- Don't pass HTTP request context to background goroutines — it cancels when the response is sent. Use
- `context.WithoutCancel(r.Context())` (Go 1.21+) for fire-and-forget background work.
-
- ### Synchronization Primitives
-
- - **Mutex:** zero-value is valid. Use named field `mu sync.Mutex`, never embed. Never copy. Use `defer mu.Unlock()`
- unless nanosecond performance matters.
- - **Atomics:** for simple flags/counters, prefer `sync/atomic` types (`atomic.Bool`, `atomic.Int64`).
-
- ### Data Race Gotchas
-
- - **Append on shared slices:** `append` isn't data-race-free when slice has spare capacity. Copy before passing to
- goroutines.
- - **Map/slice assignment doesn't copy:** both variables point to same backing storage. Deep copy inside critical section
- (`maps.Clone`).
- - **String formatting deadlocks:** `fmt.Errorf("%v", obj)` may call `obj.String()`, which may lock the same mutex.
- Validate before locking, or format with direct field access.
-
- ### Concurrency Rules
+ - **Every goroutine needs an exit condition and a way to wait for it.** No fire-and-forget: a goroutine nobody joins is
+ a leak with a race attached.
+ - **No goroutines in `init()`.** Spawn them from a constructor that owns the lifecycle.
+ - **`context.Context` is the default cancellation mechanism.** Explicit stop and done channels belong only in code that
+ predates it.
+ - **`wg.Go(f)` over `wg.Add(1)` plus `defer wg.Done()`** — the pair is where counter mistakes come from.
+ - **`errgroup.WithContext` over a hand-rolled WaitGroup and error collection.**
+ - **Channel buffer is zero or one.** A larger buffer requires knowing what stops it from filling.
+ - **Declare channel direction in signatures** (`<-chan`, `chan<-`) and close only from the sending side.
+ - **`select` picks a ready case at random**, not in source order. Drain the work channel after the stop signal where
+ priority matters.
+ - **A nil channel blocks forever** — assign `nil` to a channel to remove its case from a `select` at runtime.
+ - **Mutexes are named fields, never embedded, never copied.** `defer mu.Unlock()` unless nanoseconds are measured. Reach
+ for `atomic.Bool` and `atomic.Int64` for flags and counters.
+ - **`fmt.Errorf("%v", obj)` can deadlock** — it may call `obj.String()`, which may take the mutex you already hold.
+ Format from fields you have already read.
+ - **Clone with `maps.Clone` inside the critical section.** Assigning a map or a slice shares the backing storage, so
+ both variables see every write.
- - No goroutines in `init()` — spawn in constructors with lifecycle management.
- - Use `select` with done/context for cancellable operations.
+ Read [`${CLAUDE_SKILL_DIR}/references/conventions/concurrency.md`] when building a pipeline, a fan-out, a fan-in, or a
+ bounded worker pool — it carries the full pattern for each and the worker-with-lifecycle template.
## Testing
- ### Table-Driven Tests
-
- - Slice named `tests`, each case `tt`
- - Inputs prefixed `give`, outputs prefixed `want`
- - Always use `t.Run` with descriptive names
- - Use field names in struct literals
- - Omit zero-value fields unless they add context
- - Every row must use every field — uniform logic only
-
- ### When NOT to Use Table Tests
-
- Split into separate `Test...` functions when: different cases need different setup/mocking, conditional assertions
- inside the loop, complex mock configuration per case, or table fields used only by some cases.
-
- ### Subtests
-
- `t.Run` creates subtests: `t.Fatal` stops only the current subtest, run individually with `go test -run=TestX/case`,
- shared setup/teardown via parent function.
-
- ### Parallel Tests
-
- Call `t.Parallel()` in subtests. In Go 1.22+, `tt` is safe in the closure. For Go < 1.22, shadow: `tt := tt`. Group
- parallel subtests with teardown by nesting under an intermediate `t.Run("group", ...)`.
-
- ### Assertions (testify)
-
- - **`require`** — stops test on failure. Use for error checks and nil guards.
- - **`assert`** — reports failure, continues. Use for independent value checks.
- - `require.Equal`/`assert.Equal` for struct and slice comparison — never `reflect.DeepEqual` directly.
- - `assert.ElementsMatch` for order-independent slice comparison.
-
- ### Test Error Semantics
-
- - Prefer `errors.Is` / `errors.As` for semantic matching
- - `require.ErrorContains` for substring when no sentinel available
- - Never use exact string matching on error messages
-
- ### t.Error vs t.Fatal
-
- Prefer `t.Error` to report all failures at once. Use `t.Fatal` only for setup failures or when a check makes subsequent
- checks impossible.
-
- ### t.Fatal in Goroutines
-
- `t.Fatal`/`t.Fatalf`/`t.FailNow` must only be called from the goroutine running the test function. Use `t.Errorf` +
- `return` in spawned goroutines. Note: `t.Parallel()` does NOT create a new goroutine — `t.Fatal` is safe in parallel
- subtests.
-
- ### Test Helpers
-
- Mark with `t.Helper()` so failures report the caller's line. Don't use `t.Helper()` in assert-like wrappers — it hides
- the connection between failure and cause.
-
- ### Test Double Package Naming
-
- Name by appending `test` to production package: `creditcardtest`. Use simple names (`Stub`, `Fake`) when only one type
- needs doubling; prefix with type name (`StubService`, `StubStoredValue`) when multiple. Prefix test double variables:
- `var spyCC creditcardtest.Spy`.
-
- ### Scoped Test Setup
-
- Keep setup scoped to tests that need it. Don't use `init()` or package-level vars for test data. Use `sync.Once` for
- expensive setup shared across tests.
-
- ### Test Cache Safety
-
- Go's test cache uses file mtime and env values. Never write to source directory in tests — use `t.TempDir()` for temp
- files and `t.Setenv()` for environment variables.
-
- ### Live Services Over Mocks
-
- Prefer real service instances (databases, caches, brokers) over synthetic mocks. Gate slow tests behind environment
- variables and skip when not set.
-
- ### Test Naming and Organization
-
- - `Test_TypeName` with underscore for type-level tests, `t.Run()` for method/scenario
- - Black-box preferred: `package foo_test` in `foo_test.go`
- - White-box when needed: `package foo` in `foo_internal_test.go`
- - Benchmark files: `foo_benchmark_test.go` or `foo_benchmark_internal_test.go`
-
- ### Block Scoping
-
- Use bare blocks `{}` for logical grouping when separate test reporting is unnecessary. Use `t.Run()` when you need
- parallel execution, selective running, or per-scenario reporting.
-
- ### Complementary Operations
-
- Test complementary operations together (Put + Get) when it reduces duplication. Split only when operations have
- independent failure modes.
-
- ### Compare Stable Results
-
- Don't assert on serialization output — parse and compare semantically. Never depend on `json.Marshal` field ordering.
-
- ### Runnable Examples
-
- Write `func Example...` for complex APIs — godoc renders them, `go test` verifies them. The `// Output:` comment makes
- the example a test.
-
- ### Race Detection
-
- Always run tests with `-race` for concurrent code. Enable in CI. Use `//go:build !race` to exclude specific files if
- needed.
-
- ### Avoid Sleeping
-
- `time.Sleep` in tests creates flaky tests. Use channels, WaitGroups, or polling with timeout. If synchronization is
- impossible, use a retry/poll loop with deadline.
-
- ### Testing Utilities
-
- - `httptest.NewRequest`/`httptest.NewRecorder` for in-process HTTP handler testing
- - `httptest.NewServer` for testing clients against fake servers
- - `testing/iotest.ErrReader` for error-injecting readers
- - `testing/iotest.OneByteReader` for one-byte-at-a-time reads
-
- ### Benchmarks
+ - **Table tests**: slice named `tests`, case named `tt`, inputs prefixed `give`, expectations prefixed `want`, every
+ case under `t.Run` with a descriptive name. Every row must exercise every field.
+ - **Split into separate test functions** where cases need different setup, conditional assertions, or per-case mock
+ configuration. A table whose fields only some rows use is two tests.
+ - **`require` for anything that makes the rest of the test meaningless** — error checks, nil guards. `assert` for
+ independent value checks. `require.Equal` for structs and slices, never `reflect.DeepEqual`.
+ - **Never match an error by message text.** Use `errors.Is` or `errors.As`, and `require.ErrorContains` only where no
+ sentinel exists.
+ - **`t.Fatal` only from the goroutine running the test.** Use `t.Errorf` and return in a spawned goroutine.
+ `t.Parallel()` does not spawn one, so `t.Fatal` stays safe in a parallel subtest.
+ - **`t.Helper()` in setup helpers, never in an assertion wrapper** — there it hides the line that failed.
+ - **Use the self-undoing scoped helpers**: `t.TempDir()`, `t.Setenv()`, `t.Chdir()`, `t.Context()`, `t.Cleanup()`. A
+ test that writes into its source directory or mutates the environment poisons the build cache, which keys on both.
+ - **Never `time.Sleep`.** For behavior that depends on time, run the test in a `synctest` bubble:
+ `synctest.Test(t, func(t *testing.T) { ... })` gives it a fake clock that jumps forward the moment every goroutine in
+ the bubble blocks, so a one-hour timeout resolves instantly. `synctest.Wait` blocks until the others are blocked.
+ - **`-race` on every run of concurrent code**, locally and in CI.
+ - **Prefer real service instances to mocks** — databases, caches, brokers. Gate the slow ones behind an environment
+ variable and skip when it is unset.
+ - **Never assert on serialized output.** Parse and compare semantically; `json.Marshal` field order is not a contract.
+ - **Naming**: `Test_TypeName` for type-level tests, with `t.Run` per method or scenario. Black-box `package foo_test` by
+ default; `package foo` in `foo_internal_test.go` where internals must be reached. Benchmarks in
+ `foo_benchmark_test.go`.
+ - **Test doubles live in a package named for their subject plus `test`** — `creditcardtest` — with plain type names
+ (`Stub`, `Fake`) where one type is doubled and prefixed names (`StubService`) where several are.
+ - **`for b.Loop()` for a benchmark loop.** The body runs once per `-count`, so setup outside it runs once, and arguments
+ and results stay alive without a package-level sink.
- - Use `b.Loop()` (Go 1.24+) or `for i := 0; i < b.N; i++`
- - `b.ResetTimer()` after expensive setup
- - `b.ReportAllocs()` to track allocations
- - Assign result to package-level var to prevent compiler elimination
- - Use `-benchtime=5s` or `benchstat` for stable micro-benchmarks
+ Read [`${CLAUDE_SKILL_DIR}/references/conventions/testing.md`] when writing the first test in a package, or when a test
+ needs `httptest` or `iotest` — it carries the table-test template, the parallel-with-teardown shape, and the utility
+ catalog.
## Project Structure
- ### Layout Principles
-
- - **`internal/` for encapsulation.** All server logic, supporting packages not part of public API. Refactor freely
- without breaking external consumers.
- - **`cmd/` for commands.** Each subdirectory declares `package main`. Install with `go install .../cmd/tool@latest`.
- - **Start flat.** Add directories only when a package needs internal helpers, multiple commands exist, or sub-packages
- serve distinct importable purposes.
-
- ### Package Design
-
- - Lowercase, no underscores: `userstore` not `user_store`
- - Singular: `user` not `users`
- - By purpose: `auth`, `cache`, `handler`
-
- ### Imports
-
- Two groups separated by blank line: (1) standard library, (2) everything else. Alias only to avoid conflicts. Blank
- imports (`import _ "pkg"`) only in `main` packages or tests. Dot imports only in test files to resolve circular
- dependencies.
-
- ### Function Organization
-
- Within a file, order by: (1) types, constants, variables, (2) constructor (`New...`), (3) exported methods grouped by
- receiver, (4) unexported methods grouped by receiver, (5) utility functions. Order by rough call order — callers before
- callees.
-
- ### File Organization
-
- - One file per major type (for large types)
- - Test file adjacent: `foo.go` -> `foo_test.go`
- - Keep related code together — don't scatter features across files
- - `doc.go` for package-level documentation if needed
- - Kebab-case for Go source files: `user-service.go`, `http-handler.go`
-
- ### Backward-Incompatible Changes
-
- Staged workflow: (1) add new code without touching old, (2) migrate callers, (3) remove old code. Each step is a
- separate commit. Never combine breaking changes with new functionality.
-
- ## Configuration Patterns
-
- For constructors with 3+ optional parameters, choose between option structs and functional options.
+ - **`internal/` by default.** Everything not deliberately part of the public API goes there, where it can be refactored
+ without breaking anyone.
+ - **`cmd/<name>/` per binary**, holding `package main` and nothing else.
+ - **Start flat.** Add a directory when a package needs helpers of its own, a second command appears, or a sub-package
+ serves a distinct importable purpose — never in anticipation.
+ - **Two import groups** — standard library, then everything else — separated by a blank line. Alias only to resolve a
+ collision. Blank imports in `main` packages and tests only; dot imports in test files only.
+ - **Order a file by call order**: types and constants, constructor, exported methods grouped by receiver, unexported
+ methods, helpers. Callers before callees.
+ - **Break compatibility in three commits** — add the new form, migrate the callers, delete the old form. Never combine a
+ breaking change with new functionality.
- ### Option Structs
+ Read [`${CLAUDE_SKILL_DIR}/references/conventions/structure.md`] when creating a package or making a
+ backward-incompatible change — it carries the staged workflow and the package-naming examples.
- Use when most callers need several options, or options are shared across functions. Benefits: self-documenting field
- names, zero-value omission, easy to share and extend. Never include `context.Context` in option structs.
+ ## Configuration
- ### Functional Options
+ For a constructor with three or more optional parameters, pick one shape and hold it across the package.
- Use when most callers need zero options, there are many options, or options require validation. Use the interface form
- (`type Option interface{ apply(*options) }`) over closures for testability. Options should accept parameters, not use
- presence as signal: `rpc.FailFast(true)` not `rpc.EnableFailFast()`.
+ - **Option struct** where most callers set several options, or the options are shared across functions.
+ - **Functional options** where most callers set none, the option count is large, options need validation, or third
+ parties must extend the set. Use the interface form, `type Option interface{ apply(*options) }`, and give every option
+ a parameter: `rpc.FailFast(true)`, never `rpc.EnableFailFast()`.
- ### Decision Criteria
+ Read [`${CLAUDE_SKILL_DIR}/references/conventions/idioms.md`] when the choice is contested, or when a naming,
+ declaration, or embedding decision needs a worked example — it carries the decision table across both shapes.
- | Factor | Option Struct | Functional Options |
- | --------------------------------- | ------------- | ------------------ |
- | Most callers need several options | Prefer | Either |
- | Most callers need zero options | Either | Prefer |
- | Options need validation | Either | Prefer |
- | Options shared across functions | Prefer | Either |
- | Third-party extensibility needed | Avoid | Prefer |
+ ## Types and Zero Values
- ## Zero-Value Design
+ - **Design the zero value to be useful.** `var buf bytes.Buffer` is ready to write to. Add a constructor only where a
+ non-zero default is required.
+ - **Embed only where the promoted method set is the API you intend.** Embedding in an exported struct commits you to
+ every exported method of the inner type, including the ones it has not added yet. Use a named field otherwise.
+ - **A type alias earns its place by adding meaning**: `type UserID string` does, `type MyString string` does not.
+ - **`Run` blocks until the work is done; `Start` returns immediately and owns a goroutine.** `Start` takes a context
+ first.
- Design types so the zero value is immediately useful — no constructor needed. `var buf bytes.Buffer` is ready to use.
- Only write constructors when non-zero defaults are required.
+ ## Standard Library
- ## Embedding
+ - **`slices` and `maps` over hand-written loops.** `slices.Contains`, `slices.SortFunc`, `slices.BinarySearch`,
+ `slices.Compact`, `maps.Clone`, `maps.DeleteFunc`. A loop reimplementing one of these is a defect, not a style choice.
+ Comparison helpers come from `cmp`.
+ - **`log/slog` for structured logging.** Reserve `log` for a binary's fatal path.
+ - **Iterators for sequences.** Return `iter.Seq[V]` or `iter.Seq2[K, V]` where the caller consumes elements one at a
+ time and the whole set need not exist at once; return a slice where it is small and already materialized. Collect with
+ `slices.Collect`, `slices.Sorted`, or `maps.Collect` — `maps.Keys` yields an iterator, so the ordered key slice is
+ `slices.Sorted(maps.Keys(m))`.
+ - **Writing an iterator: stop as soon as `yield` returns false**, and release resources with `defer` inside the iterator
+ function.
+ - **`os.Root` for any path that comes from outside the program.** Its methods refuse to escape the directory, symlinks
+ included. `filepath.Clean` does not.
+ - **`math/rand/v2` over `math/rand`** for non-cryptographic randomness; `crypto/rand` where a value must be
+ unpredictable.
- Embedding promotes methods of the inner type to the outer type. Use embedding when promoted methods ARE your intended
- API. Use named fields when you don't want to expose the inner type's full method set. Never embed in public API structs
- unless the promoted surface is intentional — it commits your API to every exported method including future additions.
- Embedding in `internal/` types is lower risk.
+ ## Documentation
- ## Long-Running Process Naming
+ - **Every exported symbol gets a doc comment starting with its name**, in complete sentences. Package documentation
+ lives in `doc.go`.
+ - **Unexported symbols get none by default.** No external caller reads them, so non-obvious behavior there is a naming
+ or shape problem first.
+ - **Update the doc comment in the edit that changes the symbol**, not afterwards.
- - **Run** — blocks until process completes. Caller controls the goroutine.
- - **Start** — returns immediately, spawns internal goroutine. Accept `context.Context` as first parameter for
- cancellation.
+ ## Global State
- ## Type Preferences
+ - **A library never forces global state.** Expose an instance API; a package-level convenience API is a thin proxy to an
+ instance and belongs only in a binary.
+ - **Global state is acceptable only where it is logically constant, stateless, or free of external side effects.**
- - Prefer `any` over `interface{}` (Go 1.18+). Only use `any` when truly accepting any type.
- - Use type aliases for semantic meaning: `type UserID string` adds type safety. `type MyString string` adds nothing.
+ ## Code Navigation
- ## Doc Comments
+ `gopls` is configured for `.go` files. Use LSP tools rather than Grep or Glob for anything that is a Go identifier —
+ they resolve imports, aliases, embedded types, and interface satisfaction, which text search cannot.
- Every exported symbol gets a doc comment starting with its name. Complete sentences, period-terminated. Package comment
- in `doc.go` or primary `.go` file. Unexported types get no doc by default -- they have no external caller, so
- non-obvious behavior is a naming or shape problem first.
+ - **Where a symbol is defined** — `goToDefinition`
+ - **Every use of a symbol** — `findReferences`
+ - **Type, signature, or doc of a symbol** — `hover`
+ - **Symbols in one file** — `documentSymbol`; **across the module** — `workspaceSymbol`
+ - **Types satisfying an interface** — `goToImplementation`
+ - **Call graph in either direction** — `incomingCalls`, `outgoingCalls`
- When modifying an exported symbol's behavior or signature, update its doc comment in the same edit.
+ Grep and Glob stay correct for comments, string literals, log messages, build tags, config values, and file-name
+ patterns. Subagents exploring Go code reach the same LSP server — instruct them to use it.
- ## Global State
+ ## Toolchain
- Libraries must not force global state. Expose instance-based APIs. Global state is safe only when logically constant,
- stateless, or has no external side effects. If providing convenience, make the global API a thin proxy to an instance
- API, and restrict to binaries — never libraries.
+ - **`golangci-lint run` must pass before a commit**, and **`golangci-lint fmt`** formats — never run `gofmt` or
+ `goimports` separately.
+ - **`go fix` after raising the `go` directive.** It applies the modernizers mechanically; review the diff as a normal
+ change.
+ - **`tool` directives in `go.mod` track executable dependencies.** Add with `go get -tool`, run with `go tool <name>`.
+ Never reintroduce a `tools.go` blank-import file.
## Application
- When **writing** Go code: apply all conventions silently — don't narrate each rule. If an existing codebase contradicts
- a convention, follow the codebase and flag the divergence.
+ When **writing** Go, apply these conventions silently — do not narrate a rule while following it. Where existing code
+ contradicts one, follow the codebase and flag the divergence once.
- When **reviewing** Go code: cite the specific violation and show the fix inline. Don't lecture — state what's wrong and
- how to fix it.
+ When **reviewing** Go, cite the violation and show the fix inline. Do not lecture.
```
Bad: "According to Go conventions, error strings should be lowercase..."
- Good: "errors.New("Not found.") -> errors.New("not found")"
+ Good: errors.New("Not found.") -> errors.New("not found")
```
- ## Code Navigation — LSP Required
-
- A `gopls` LSP server is configured for `.go` files. **Always use LSP tools for code navigation instead of Grep or
- Glob.** LSP understands Go's type system, scope rules, and module boundaries — text search does not.
-
- ### Tool Routing
-
- - **Find where a function/type/method is defined** — `goToDefinition` (resolves imports, aliases, embedded types)
- - **Find all usages of a symbol** — `findReferences` (scope-aware, no false positives from string matches)
- - **Get type signature, docs, or return types** — `hover` (instant type info without reading source files)
- - **List all symbols in a file** — `documentSymbol` (structured output vs grepping for `func`/`type`)
- - **Find a symbol by name across the project** — `workspaceSymbol` (searches all packages)
- - **Find concrete types implementing an interface** — `goToImplementation` (knows the type system and implicit
- interfaces)
- - **Find what calls a function** — `incomingCalls` (precise call graph across module boundaries)
- - **Find what a function calls** — `outgoingCalls` (structured dependency map)
-
- **Grep/Glob remain appropriate for:** text in comments, string literals, log messages, TODO markers, config values,
- build tags, file name patterns — anything that isn't a Go identifier.
-
- When spawning subagents for Go codebase exploration, instruct them to use LSP tools. Subagents have access to the same
- LSP server.
-
- ## Toolchain
-
- - **`golangci-lint`**: single entry point for formatting and linting. Configure per project.
- - `golangci-lint run` — lint. Must pass before committing.
- - `golangci-lint fmt` — format. Use instead of running `gofmt`/`goimports` separately.
-
## Integration
- The **coding** skill governs workflow (discovery, planning, verification); this skill governs Go implementation choices.
- Both are active simultaneously.
+ The **coding** skill governs workflow — discovery, planning, verification. This skill governs Go implementation choices,
+ and wins on any question of how Go code should read. Both are active at once.
- **Simplicity is the highest Go virtue. When in doubt, write boring code.**
+ The **templ**, **zog**, and **charm-tui** skills own their library stacks and defer to this skill for language
+ conventions.
+
+ **When in doubt, write boring code.**