Simplicity is the highest Go virtue. Three biases decide most calls:
- 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.
Language Version
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.
Floor version per feature. Anything older than 1.21 is available in every supported module.
- 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
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.
Naming
- 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
- 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).
Generics
- 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.
Receivers
- 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
- 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
- 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
- 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.
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
:= 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.
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
- 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.
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 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.
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
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.
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.
Configuration
For a constructor with three or more optional parameters, pick one shape and hold it across the package.
- 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().
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.
Types and Zero Values
- 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.
Standard Library
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.
Documentation
- 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.
Global State
- 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.
Code Navigation
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.
- 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
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.
Toolchain
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, 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, 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")
Integration
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.
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.
1---2name: golang3description: Write and review Go: idioms, error handling, concurrency, testing, project structure, the toolchain, and the language version a module's `go` directive permits.4---56Simplicity is the highest Go virtue. Three biases decide most calls:78- **Resist abstraction until the cost of not abstracting is proven.** An interface, a type parameter, or a layer added9 in anticipation of a second caller is a defect.10- **Reach for the standard library first.** A dependency earns its place by doing what the standard library does not.11- **The `go` directive decides what may be written**, not the installed toolchain.1213## Language Version1415The `go` directive in `go.mod` sets the language version a module may use. Read it before writing code and never reach16for a feature above it. The `stdversion` analyzer reports a standard-library symbol newer than the directive: Go 1.2717and later fail `go test` on it, earlier toolchains report it under `go vet`.1819Floor version per feature. Anything older than 1.21 is available in every supported module.2021- **1.21** — `min`, `max`, `clear` builtins; `slices`, `maps`, `cmp`, `log/slog`; `context.WithoutCancel`;22 `errors.ErrUnsupported`; `sync.OnceValue`23- **1.22** — per-iteration loop variables; `for range int`; `math/rand/v2`; method and wildcard patterns in24 `net/http.ServeMux`25- **1.23** — range-over-func iterators; `iter`; iterator functions in `slices` and `maps`; `unique`; unbuffered timer26 channels27- **1.24** — generic type aliases; `os.Root`; `testing.B.Loop`; `t.Context`; `runtime.AddCleanup`; `omitzero` struct28 tag; `tool` directives; iterator functions in `strings`29- **1.25** — `testing/synctest`; `sync.WaitGroup.Go`; container-aware `GOMAXPROCS`30- **1.26** — `new(expr)`; self-referential type constraints; `errors.AsType`; `go fix` modernizers31- **1.27** — generic methods; field-selector keys in struct literals; `encoding/json/v2`; `uuid`; `strings.CutLast`3233Read [`${CLAUDE_SKILL_DIR}/references/versions/go1.NN.md`] — one file per version, `go1.21.md` through `go1.27.md` —34when writing against a feature near the module's floor, and whenever raising the directive. Each carries what its35version added, which behavior changes the `go` directive gates, and the traps it introduced.3637## Naming3839- **Never `util`, `common`, `misc`, `shared`, `helpers`, or `types` as a package name.** Name a package for what it40 provides; one that resists naming is holding unrelated things.41- **Do not stutter.** The package name prefixes every qualified reference: `widget.New()`, never `widget.NewWidget()`.42- **Go source files are kebab-case**: `user-service.go`, `http-handler.go`.4344## Interfaces4546- **Define an interface where it is consumed**, never where it is implemented. Producers return concrete types.47- **Never define an interface for mocking.** Wait for a second implementation or a real consumer boundary.48- **Assert satisfaction at compile time** where the contract matters: `var _ http.Handler = (*Handler)(nil)`.4950## Generics5152- **Write the concrete version first.** Add type parameters when two or more implementations differ only by type — not53 in anticipation of a second caller.54- **Constrain by what the body uses.** `cmp.Ordered` when it compares, `any` when it only moves values. A constraint55 listing methods the function never calls is noise.56- **Prefer a type parameter to `any` plus a type switch** when callers know the type. It moves the failure to compile57 time and removes the assertion.58- **A function generic in a type it passes through unchanged is a function on `any`.** Drop the parameter.59- **Methods may declare their own type parameters** — put a generic operation in the type's namespace when it belongs60 there rather than the package's. Interface methods cannot be generic, so a generic method never satisfies an61 interface.6263## Receivers6465- **Never mix pointer and value receivers on one type.** Default to pointer; take a value receiver only for a small66 immutable value type.67- **Never take a pointer to a map, func, or channel** — they are already reference types.6869## Context7071- **Never store a `context.Context` in a struct or an option struct.** Pass it as the first parameter.72- **`context.Background()` only in `main()` and test setup.**73- **Never hand a request context to background work** — it is canceled when the response is sent. Use74 `context.WithoutCancel(r.Context())`.7576## Declarations7778- **Nil slices are valid and preferred**: `var s []string`, tested with `len(s) == 0`. Use `[]string{}` only where the79 JSON encoding must be `[]` rather than `null`. Pre-allocate when the size is known: `make([]T, 0, n)`.80- **`&T{}` for a pointer to a struct, `new(expr)` for a pointer to any other value** — `new(expr)` replaces the81 hand-rolled `ptr(v)` helper.82- **Start `iota` at 1** unless the zero value carries meaning: `const ( StatusActive Status = iota + 1; ... )`.83- **Name result parameters only where they document a caller obligation**, never to enable a naked return.8485## Error Handling8687- **Handle an error once — log it or return it, never both.** Logging and returning reports one failure at every level88 of the stack. If you log, degrade and stop; if you return, wrap and let the caller decide.89- **Wrap with context the underlying error does not already carry**: `fmt.Errorf("get user: %w", err)`. Lowercase, no90 trailing punctuation, no "failed to" prefix, `%w` last so the chain reads newest to oldest.91- **Use `%v` instead of `%w` when the wrapped error is an implementation detail** — `%w` commits your API to the92 dependency that produced it.93- **Match with `errors.Is` and `errors.As`, never `==` or a bare type assertion.** `errors.AsType[T]` is the generic94 form and needs no pre-declared target.95- **Wrap a sentinel before returning it** so callers cannot match it with `==`.96- **`errors.Join` only where every operation must be attempted** — closing a set of resources, validating every field.97 Never as a substitute for returning at the first failure on a sequential path.98- **Never silently drop an error from a deferred call.** Propagate a close error when no prior error exists; write99 `_ = f.Close()` where the drop is deliberate.100- **`MustXxx` panics only in package-level initialization and test helpers**, never in a request path.101- **Prefer the project's structured error package** — `ErrNotFound.Wrap(err)`, `e.NewFrom("context", err)` — over102 `fmt.Errorf` wherever one is in use, and use it for every error in that project.103104Read [`${CLAUDE_SKILL_DIR}/references/conventions/errors.md`] when the project uses golib/e, when designing a new error105type, or when a package needs internal panic/recover — it carries the golib/e API, the creation decision tree, and the106recover-at-the-boundary pattern.107108## Gotchas109110- **`:=` in an inner block silently shadows the outer variable**, `err` most often. Assign with `=` when the target is111 an outer variable.112- **`defer` evaluates its arguments immediately.** Capture later values in a closure: `defer func() { notify(s) }()`.113- **`defer` fires at function return, not at the end of a loop iteration.** Extract the loop body into a function.114- **`append` to a slice with spare capacity mutates the shared backing array**, and every slice over that array sees it.115 Cap with `s[:len(s):len(s)]` or copy.116- **A nil pointer in an interface is not nil.** Return an explicit `nil` from a function whose result type is an117 interface, never a typed nil.118- **Never copy a `sync.Mutex` or a struct holding one.** Copy slices and maps at API boundaries so callers cannot mutate119 your state.120- **Prefer `int`.** Sized integer types overflow silently, and only a protocol, a binary format, or a measured121 constraint justifies one.122- **`+=` in a loop is O(n²).** Use `strings.Builder` with `Grow`.123- **Name an inverted condition instead of commenting it**: `if cacheHit := err == nil; cacheHit {`. A name is read at124 the branch and cannot go stale.125126Read [`${CLAUDE_SKILL_DIR}/references/conventions/gotchas.md`] when one of these bites and the obvious fix does not hold127— it carries an annotated reproduction and fix for each.128129## Concurrency130131- **Every goroutine needs an exit condition and a way to wait for it.** No fire-and-forget: a goroutine nobody joins is132 a leak with a race attached.133- **No goroutines in `init()`.** Spawn them from a constructor that owns the lifecycle.134- **`context.Context` is the default cancellation mechanism.** Explicit stop and done channels belong only in code that135 predates it.136- **`wg.Go(f)` over `wg.Add(1)` plus `defer wg.Done()`** — the pair is where counter mistakes come from.137- **`errgroup.WithContext` over a hand-rolled WaitGroup and error collection.**138- **Channel buffer is zero or one.** A larger buffer requires knowing what stops it from filling.139- **Declare channel direction in signatures** (`<-chan`, `chan<-`) and close only from the sending side.140- **`select` picks a ready case at random**, not in source order. Drain the work channel after the stop signal where141 priority matters.142- **A nil channel blocks forever** — assign `nil` to a channel to remove its case from a `select` at runtime.143- **Mutexes are named fields, never embedded, never copied.** `defer mu.Unlock()` unless nanoseconds are measured. Reach144 for `atomic.Bool` and `atomic.Int64` for flags and counters.145- **`fmt.Errorf("%v", obj)` can deadlock** — it may call `obj.String()`, which may take the mutex you already hold.146 Format from fields you have already read.147- **Clone with `maps.Clone` inside the critical section.** Assigning a map or a slice shares the backing storage, so148 both variables see every write.149150Read [`${CLAUDE_SKILL_DIR}/references/conventions/concurrency.md`] when building a pipeline, a fan-out, a fan-in, or a151bounded worker pool — it carries the full pattern for each and the worker-with-lifecycle template.152153## Testing154155- **Table tests**: slice named `tests`, case named `tt`, inputs prefixed `give`, expectations prefixed `want`, every156 case under `t.Run` with a descriptive name. Every row must exercise every field.157- **Split into separate test functions** where cases need different setup, conditional assertions, or per-case mock158 configuration. A table whose fields only some rows use is two tests.159- **`require` for anything that makes the rest of the test meaningless** — error checks, nil guards. `assert` for160 independent value checks. `require.Equal` for structs and slices, never `reflect.DeepEqual`.161- **Never match an error by message text.** Use `errors.Is` or `errors.As`, and `require.ErrorContains` only where no162 sentinel exists.163- **`t.Fatal` only from the goroutine running the test.** Use `t.Errorf` and return in a spawned goroutine.164 `t.Parallel()` does not spawn one, so `t.Fatal` stays safe in a parallel subtest.165- **`t.Helper()` in setup helpers, never in an assertion wrapper** — there it hides the line that failed.166- **Use the self-undoing scoped helpers**: `t.TempDir()`, `t.Setenv()`, `t.Chdir()`, `t.Context()`, `t.Cleanup()`. A167 test that writes into its source directory or mutates the environment poisons the build cache, which keys on both.168- **Never `time.Sleep`.** For behavior that depends on time, run the test in a `synctest` bubble:169 `synctest.Test(t, func(t *testing.T) { ... })` gives it a fake clock that jumps forward the moment every goroutine in170 the bubble blocks, so a one-hour timeout resolves instantly. `synctest.Wait` blocks until the others are blocked.171- **`-race` on every run of concurrent code**, locally and in CI.172- **Prefer real service instances to mocks** — databases, caches, brokers. Gate the slow ones behind an environment173 variable and skip when it is unset.174- **Never assert on serialized output.** Parse and compare semantically; `json.Marshal` field order is not a contract.175- **Naming**: `Test_TypeName` for type-level tests, with `t.Run` per method or scenario. Black-box `package foo_test` by176 default; `package foo` in `foo_internal_test.go` where internals must be reached. Benchmarks in177 `foo_benchmark_test.go`.178- **Test doubles live in a package named for their subject plus `test`** — `creditcardtest` — with plain type names179 (`Stub`, `Fake`) where one type is doubled and prefixed names (`StubService`) where several are.180- **`for b.Loop()` for a benchmark loop.** The body runs once per `-count`, so setup outside it runs once, and arguments181 and results stay alive without a package-level sink.182183Read [`${CLAUDE_SKILL_DIR}/references/conventions/testing.md`] when writing the first test in a package, or when a test184needs `httptest` or `iotest` — it carries the table-test template, the parallel-with-teardown shape, and the utility185catalog.186187## Project Structure188189- **`internal/` by default.** Everything not deliberately part of the public API goes there, where it can be refactored190 without breaking anyone.191- **`cmd/<name>/` per binary**, holding `package main` and nothing else.192- **Start flat.** Add a directory when a package needs helpers of its own, a second command appears, or a sub-package193 serves a distinct importable purpose — never in anticipation.194- **Two import groups** — standard library, then everything else — separated by a blank line. Alias only to resolve a195 collision. Blank imports in `main` packages and tests only; dot imports in test files only.196- **Order a file by call order**: types and constants, constructor, exported methods grouped by receiver, unexported197 methods, helpers. Callers before callees.198- **Break compatibility in three commits** — add the new form, migrate the callers, delete the old form. Never combine a199 breaking change with new functionality.200201Read [`${CLAUDE_SKILL_DIR}/references/conventions/structure.md`] when creating a package or making a202backward-incompatible change — it carries the staged workflow and the package-naming examples.203204## Configuration205206For a constructor with three or more optional parameters, pick one shape and hold it across the package.207208- **Option struct** where most callers set several options, or the options are shared across functions.209- **Functional options** where most callers set none, the option count is large, options need validation, or third210 parties must extend the set. Use the interface form, `type Option interface{ apply(*options) }`, and give every option211 a parameter: `rpc.FailFast(true)`, never `rpc.EnableFailFast()`.212213Read [`${CLAUDE_SKILL_DIR}/references/conventions/idioms.md`] when the choice is contested, or when a naming,214declaration, or embedding decision needs a worked example — it carries the decision table across both shapes.215216## Types and Zero Values217218- **Design the zero value to be useful.** `var buf bytes.Buffer` is ready to write to. Add a constructor only where a219 non-zero default is required.220- **Embed only where the promoted method set is the API you intend.** Embedding in an exported struct commits you to221 every exported method of the inner type, including the ones it has not added yet. Use a named field otherwise.222- **A type alias earns its place by adding meaning**: `type UserID string` does, `type MyString string` does not.223- **`Run` blocks until the work is done; `Start` returns immediately and owns a goroutine.** `Start` takes a context224 first.225226## Standard Library227228- **`slices` and `maps` over hand-written loops.** `slices.Contains`, `slices.SortFunc`, `slices.BinarySearch`,229 `slices.Compact`, `maps.Clone`, `maps.DeleteFunc`. A loop reimplementing one of these is a defect, not a style choice.230 Comparison helpers come from `cmp`.231- **`log/slog` for structured logging.** Reserve `log` for a binary's fatal path.232- **Iterators for sequences.** Return `iter.Seq[V]` or `iter.Seq2[K, V]` where the caller consumes elements one at a233 time and the whole set need not exist at once; return a slice where it is small and already materialized. Collect with234 `slices.Collect`, `slices.Sorted`, or `maps.Collect` — `maps.Keys` yields an iterator, so the ordered key slice is235 `slices.Sorted(maps.Keys(m))`.236- **Writing an iterator: stop as soon as `yield` returns false**, and release resources with `defer` inside the iterator237 function.238- **`os.Root` for any path that comes from outside the program.** Its methods refuse to escape the directory, symlinks239 included. `filepath.Clean` does not.240- **`math/rand/v2` over `math/rand`** for non-cryptographic randomness; `crypto/rand` where a value must be241 unpredictable.242243## Documentation244245- **Every exported symbol gets a doc comment starting with its name**, in complete sentences. Package documentation246 lives in `doc.go`.247- **Unexported symbols get none by default.** No external caller reads them, so non-obvious behavior there is a naming248 or shape problem first.249- **Update the doc comment in the edit that changes the symbol**, not afterwards.250251## Global State252253- **A library never forces global state.** Expose an instance API; a package-level convenience API is a thin proxy to an254 instance and belongs only in a binary.255- **Global state is acceptable only where it is logically constant, stateless, or free of external side effects.**256257## Code Navigation258259`gopls` is configured for `.go` files. Use LSP tools rather than Grep or Glob for anything that is a Go identifier —260they resolve imports, aliases, embedded types, and interface satisfaction, which text search cannot.261262- **Where a symbol is defined** — `goToDefinition`263- **Every use of a symbol** — `findReferences`264- **Type, signature, or doc of a symbol** — `hover`265- **Symbols in one file** — `documentSymbol`; **across the module** — `workspaceSymbol`266- **Types satisfying an interface** — `goToImplementation`267- **Call graph in either direction** — `incomingCalls`, `outgoingCalls`268269Grep and Glob stay correct for comments, string literals, log messages, build tags, config values, and file-name270patterns. Subagents exploring Go code reach the same LSP server — instruct them to use it.271272## Toolchain273274- **`golangci-lint run` must pass before a commit**, and **`golangci-lint fmt`** formats — never run `gofmt` or275 `goimports` separately.276- **`go fix` after raising the `go` directive.** It applies the modernizers mechanically; review the diff as a normal277 change.278- **`tool` directives in `go.mod` track executable dependencies.** Add with `go get -tool`, run with `go tool <name>`.279 Never reintroduce a `tools.go` blank-import file.280281## Application282283When **writing** Go, apply these conventions silently — do not narrate a rule while following it. Where existing code284contradicts one, follow the codebase and flag the divergence once.285286When **reviewing** Go, cite the violation and show the fix inline. Do not lecture.287288```289Bad: "According to Go conventions, error strings should be lowercase..."290Good: errors.New("Not found.") -> errors.New("not found")291```292293## Integration294295The **coding** skill governs workflow — discovery, planning, verification. This skill governs Go implementation choices,296and wins on any question of how Go code should read. Both are active at once.297298The **templ**, **zog**, and **charm-tui** skills own their library stacks and defer to this skill for language299conventions.300301**When in doubt, write boring code.**