Go Conventions (common)
This is the base layer for Go in every a-novel / a-novel-kit repository. The rules hold
whatever the repo kind — a backend service, a shared library, or a one-off tool. Repo-kind
rules live in two companion skills; load the one matching where you work in addition to this
one:
write-go-service — clean-architecture services under a-novel (app/service-*,
app/platform-*): the cmd/internal/{config,lib,dao,core,handlers,models}/pkg layout,
the interface+implementation pattern, transactions, OpenTelemetry instrumentation, REST/gRPC
handlers, and the layer-specific test patterns.
write-go-kit — shared libraries under a-novel-kit (kit/golib, kit/jwt, …): the
golib-stays-minimal philosophy, when something earns its own package, and the public/community
API obligations that come with it.
When work spans repos and needs versions kept in sync to merge cleanly, also load
manage-versions.
Before touching any file, read it. Before touching a package, read its siblings and the
interfaces it depends on and exposes. These codebases are deliberately consistent — coherence with
the surrounding code outranks personal preference. When two files disagree, the newer one and the
one closest to your change usually win; when it is genuinely unclear, ask.
Look up the API before you use it. Before writing code that touches an external package or a
non-trivial stdlib API, check the official pkg.go.dev docs and the package's README / release
notes. APIs evolve, and the first approach that comes to mind is often not the idiomatic one.
Seconds of reading prevent subtle misuse.
After every edit
Run, in this order, after any change to Go files:
pnpm generate:go # ONLY when you changed an interface or a .proto file — regenerates mocks / proto stubs
pnpm format:go # always
pnpm lint:go # always
pnpm format:go / pnpm lint:go exist in every Go repo (they wrap go mod tidy and a pinned
golangci-lint invoked through golangci-lint.mod); never skip them. If lint flags something
you did not introduce, fix it anyway while you are in the file.
When local lint fails and CI is green on the same commit, clean the cache before you believe it.
golangci-lint.mod pins the version, so local and CI run the identical linter and a disagreement is
environmental — chasing version drift is wasted effort. A stale analyzer cache is the usual cause:
facts about the standard library go stale across toolchain bumps, and once staticcheck can no
longer prove (*testing.T).Fatalf terminates, every if x == nil { t.Fatalf(…) } followed by a
deref of x reads as a nil dereference. CI never sees it because its runners start cold.
go tool -modfile=golangci-lint.mod golangci-lint cache clean
Re-run after cleaning. Findings that survive a cold run are real; editing code to appease the rest
is churn against a false positive.
Then, before the change is done:
- Invoke
write-go-tests — write or update tests for every file you created or modified, and
run the narrowest test target that covers the change (a-novel test --type=go -y, or raw
go test ./<pkg>/... for one package) until it is green. Tests are part of the change, not a
follow-up.
- Invoke
document-code — doc comments for every symbol you added or changed. Also part of
the change.
(Repo-kind skills add steps: write-go-service adds the write-openapi step when a REST
contract changes; write-go-kit adds the public-API and docs-site obligations for a graduated
package.)
Dependencies
The standing goal across both orgs: the fewest libraries doing the most. Every dependency is
a maintenance liability, a supply-chain surface, and a constraint on future choices.
- Stdlib first. If
net/http, encoding/json, context, errors, fmt, time,
crypto/*, slices, maps, cmp, etc. cover the need, use them. Never pull a package for
what the standard library already does.
- Reuse before adding. Prefer a package already in
go.mod. A second library that overlaps
one already present is almost never worth it.
- Mine what's already imported. When implementing, don't re-research online practices — the
planning phase (
plan-feature) already did that. Do read the documentation of the libraries
already in go.mod for the task at hand: a helper, option, or whole subsystem an imported
dependency already provides is code you neither write nor maintain.
- Vet candidates. A new dependency must be: well-maintained (recent commits, responsive
issue tracker, real test coverage); owned by an organization, not a single personal
account — org ownership survives a maintainer losing interest; and broad — pick the
library covering the most of the surrounding problem space, so one dependency replaces three.
A narrow utility from an individual's account is the worst combination.
- Ask first — always. Introducing a package not already in
go.mod requires explicit
developer approval. No exceptions, not even a small utility. (a-novel-kit repos hold this bar
even higher — see write-go-kit; a-novel services keep their internal/lib/ as close to
empty as possible.)
- Remove, don't accumulate. During maintenance, actively look for dependencies — and
hand-rolled helpers — that a newer upstream now subsumes, and delete the duplicate.
Package naming
One directory = one package. Package names are short, lowercase, single words — no
underscores, no camelCase: package dao, package jwk, package httpf. The name is what every
caller types in imports; keep it unambiguous, collision-free, and non-stuttering with the types
inside it (jwk.Key, not jwk.JwkKey).
File naming
Multi-word file names are camelCase — never snake_case (master_key.go ✗), never
run-together when the name is two words (masterkey.go ✗): masterKeyContext.go,
userSearch.go. A test file mirrors the production file it covers with a _test.go suffix
(userSearch.go → userSearch_test.go) — see write-go-tests for why the underscore (not a
dot) matters and how to share fixtures across packages. The repo-kind skills define the
layer/role prefixes (pg.*, rest.*, grpc.*, *.config.go, common.go, …).
Naming conventions
Variables and fields
- Be explicit.
userDao, not repo. orderCreateService, not svc. Length is not
the cost; ambiguity is.
- Short names are fine for conventional roles only:
w / r for HTTP handler params, ctx
for context.Context, err for errors, i / k / v in range loops, t for *testing.T.
- Acronyms keep ecosystem casing:
ID, URL, JSON, JWT, JWK, HTTP, REST, gRPC,
SQL, TTL. An acronym that starts an unexported identifier goes all-lowercase: id,
url, grpc.
- Don't shadow imported package names (
json, http, context, errors, time, …).
Rename the variable.
Constructors
Every type with a constructor uses New<TypeName>(deps...) *<TypeName>. Return the concrete
pointer, not an interface — unless the concrete type is genuinely an implementation detail hidden
by design (e.g. a pkg/go client behind an interface).
Always the composite-literal form, never new(T):
// WRONG — `new(T)` is a second style for the same thing; mixing the two is noise.
func NewPgUserSelect() *PgUserSelect { return new(PgUserSelect) }
// CORRECT — reads identically whether the struct is empty or has fields.
func NewPgUserSelect() *PgUserSelect { return &PgUserSelect{} }
func NewUserSearch(r UserSearchDao) *UserSearch { return &UserSearch{dao: r} }
Treat any new(T) you find as cleanup-on-sight when the file is already in scope.
Error handling
- Lowercase, no trailing punctuation. Errors get wrapped and read mid-sentence:
errors.New("user not found"), not "User not found.".
- Sentinels for expected outcomes:
var ErrUserNotFound = errors.New("user not found"),
defined in the same file as the type that produces it (or a shared file if reused across the
package). Map an upstream/library error onto your own sentinel by joining it — err = errors.Join(err, ErrUserNotFound) — so callers keep both identities.
- One vocabulary per concept. If the type is
Jwk, every error message that refers to it
says "jwk …" — never "key not found" in one file and "jwk not found" in another. Wording
drift is easy to create and hard to dashboard around. Take the term from the type name and use
it everywhere the error surfaces.
- Wrap with context and
%w: fmt.Errorf("search users: %w", err). Always %w, never
%v, so callers keep errors.Is / errors.As identity. The message chain should let a reader
trace the call path.
- Never silently discard an error. If one truly can be dropped, write
_ = ... with a
comment saying why.
Reporting errors on spans / telemetry — the layer-relative rule
When a function instruments itself with a span (or any other per-operation telemetry), one rule
governs reporting: every layer that has a span records, on its own span, every error it sees —
whether it raises it, propagates it, or maps it to a transport response. Two moves are forbidden:
suppressing reporting based on the error's identity at a propagating layer, and a bare return nil, ErrXxx from a layer that has a span. "Expected" is never a property the error value carries,
nor something one layer guesses on a caller's behalf.
- A layer that raises an error (a DAO hitting
sql.ErrNoRows, a validator producing
ErrInvalidRequest, a service detecting a mismatch) → otel.ReportError(span, err).
- A layer that receives an error and returns it upward → still
otel.ReportError. Returning
upward is propagating; wrapping it (errors.Join, fmt.Errorf("...: %w", err)) changes
nothing.
- The handler that maps the error to a transport response → still reports.
golib/httpf.HandleError
calls otel.ReportError unconditionally before writing the HTTP status, so the REST handler span
records the error whatever status it maps to; the gRPC manual mapping should do the same by hand
(_ = otel.ReportError(span, err) before status.Error(...)). The handler span exists to show
which error a request ended on.
- Anti-pattern: a helper that suppresses reporting based on the error's identity at a layer
that still propagates or surfaces it (a
reportUnexpected(span, err) keyed on a list of "known"
sentinels). It couples the layer to an error registry and silently drops real signal. The
layer-local question is just "did I see this error?" — if yes, report it.
Spans are independent — a child span ending Error does not taint the parent. So the DAO, service,
and handler spans all say "no row" for a 404, while the "is the service broken" view is built on
the HTTP status code (recorded by the otel HTTP instrumentation), which counts a 404 as a 404.
Span status answers "did an error occur in processing", which is true even for a deliberate 404,
and that is fine. For bulk-anomaly visibility on a specific security sentinel, use a counter, an
audit log, or a dedicated event rather than span.status. The helpers
otel.ReportError / otel.ReportSuccess / otel.ReportSuccessNoContent live in golib/otel;
ReportError only sets RecordError + SetStatus(Error) — it does not end the span (a
defer span.End() does), and returning a sentinel with no Report* call leaves the span Unset,
which backends treat as "completed, not a failure". write-go-service covers span naming and the
span-per-operation rule.
Context rules
- Never store
context.Context in a struct. It is request-scoped and must not outlive the
call. Pass it as the first parameter of every method that needs it.
- Contexts flow downward only — caller to callee. Never return one.
- Context values are for request-scoped data that crosses API boundaries — DB transactions,
trace spans, auth tokens. They are not a back door for optional arguments.
Time capture
When an operation derives more than one timestamp from "now" — created_at + expires_at,
start + deadline, any paired audit fields — capture now := time.Now() once and reuse it:
// WRONG — two wall-clock reads; expires_at - created_at is not exactly the TTL, and a test can't freeze it.
repo.Exec(ctx, &dao.InsertRequest{Now: time.Now(), Expiration: time.Now().Add(cfg.TTL)})
// CORRECT — one logical instant.
now := time.Now()
repo.Exec(ctx, &dao.InsertRequest{Now: now, Expiration: now.Add(cfg.TTL)})
Only call time.Now() more than once when you genuinely want distinct measurements (e.g.
computing an elapsed duration).
Secrets and sensitive data
- Never log, trace, or put in span attributes: passwords, tokens, API keys, private keys,
key ciphertexts, signed JWTs, or any other credential material. Record identifiers only
(
user.id, key.id). A redacted "*****" of the same length still leaks the input length over
every trace — don't do that either.
- Never return secret material in an error message or a transport response. Errors get
logged; responses get cached and indexed.
- Compare secrets in constant time —
crypto/subtle.ConstantTimeCompare, never == or
bytes.Equal — and on the "not found" branch of an authentication path do the equivalent work
(e.g. a throwaway hash comparison) so a lookup miss costs the same as a wrong-secret outcome
and timing does not reveal whether the subject exists.
Loop variable scope
These codebases target Go 1.22+, where for loop variables are per-iteration; a closure captures
the right value with no extra copy. Remove any current := item shadow copy inside a for range
loop when you encounter one — it is dead code on this minimum version.
Common pitfalls
context.Context stored in a struct. Always a method parameter.
- A new dependency added without asking. Explicit developer approval, every time.
- Logging / tracing secret material. Identifiers only — never the secret.
- Multiple
time.Now() for timestamps that should share one instant. Capture once, reuse.
- Suppressing span reporting for an error. Every span'd layer reports every error it sees — no
identity-keyed
reportUnexpected helper, no bare return nil, ErrXxx from a layer with a span.
new(T) in a constructor. Use &T{}.
- snake_case or run-together multi-word file names. camelCase.
- Discarding an error without
_ = and a why-comment.
- Shadowing an imported package name with a local variable. Rename the variable.
1---2name: write-go3description: Base Go conventions for EVERY Go repo in the a-novel and a-novel-kit orgs — naming, error handling, dependency policy, context, the format/lint discipline, time, and secrets. Load it for ANY Go work in either org, alongside the matching repo-kind skill: `write-go-service` (a-novel services) or `write-go-kit` (a-novel-kit libraries — `golib`, `jwt`). Pairs with `write-go-tests` and `document-code`. Not JS/TS, SQL (`write-sql`), Protobuf (`write-proto`), Dockerfiles (`write-dockerfiles`), or shell scripts (`write-bash-scripts`).4---56# Go Conventions (common)78This is the base layer for Go in every a-novel / a-novel-kit repository. The rules hold9**whatever the repo kind** — a backend service, a shared library, or a one-off tool. Repo-kind10rules live in two companion skills; load the one matching where you work **in addition to** this11one:1213- **`write-go-service`** — clean-architecture services under `a-novel` (`app/service-*`,14 `app/platform-*`): the `cmd`/`internal/{config,lib,dao,core,handlers,models}`/`pkg` layout,15 the interface+implementation pattern, transactions, OpenTelemetry instrumentation, REST/gRPC16 handlers, and the layer-specific test patterns.17- **`write-go-kit`** — shared libraries under `a-novel-kit` (`kit/golib`, `kit/jwt`, …): the18 golib-stays-minimal philosophy, when something earns its own package, and the public/community19 API obligations that come with it.2021When work spans repos and needs versions kept in sync to merge cleanly, also load22**`manage-versions`**.2324**Before touching any file, read it.** Before touching a package, read its siblings and the25interfaces it depends on and exposes. These codebases are deliberately consistent — coherence with26the surrounding code outranks personal preference. When two files disagree, the newer one and the27one closest to your change usually win; when it is genuinely unclear, ask.2829**Look up the API before you use it.** Before writing code that touches an external package or a30non-trivial stdlib API, check the official `pkg.go.dev` docs and the package's README / release31notes. APIs evolve, and the first approach that comes to mind is often not the idiomatic one.32Seconds of reading prevent subtle misuse.3334---3536## After every edit3738Run, in this order, after any change to Go files:3940```41pnpm generate:go # ONLY when you changed an interface or a .proto file — regenerates mocks / proto stubs42pnpm format:go # always43pnpm lint:go # always44```4546`pnpm format:go` / `pnpm lint:go` exist in every Go repo (they wrap `go mod tidy` and a pinned47`golangci-lint` invoked through `golangci-lint.mod`); never skip them. If `lint` flags something48you did not introduce, fix it anyway while you are in the file.4950**When local lint fails and CI is green on the same commit, clean the cache before you believe it.**51`golangci-lint.mod` pins the version, so local and CI run the identical linter and a disagreement is52environmental — chasing version drift is wasted effort. A stale analyzer cache is the usual cause:53facts about the standard library go stale across toolchain bumps, and once `staticcheck` can no54longer prove `(*testing.T).Fatalf` terminates, every `if x == nil { t.Fatalf(…) }` followed by a55deref of `x` reads as a nil dereference. CI never sees it because its runners start cold.5657```bash58go tool -modfile=golangci-lint.mod golangci-lint cache clean59```6061Re-run after cleaning. Findings that survive a cold run are real; editing code to appease the rest62is churn against a false positive.6364Then, before the change is done:65661. Invoke **`write-go-tests`** — write or update tests for every file you created or modified, and67 run the narrowest test target that covers the change (`a-novel test --type=go -y`, or raw68 `go test ./<pkg>/...` for one package) until it is green. Tests are part of the change, not a69 follow-up.702. Invoke **`document-code`** — doc comments for every symbol you added or changed. Also part of71 the change.7273(Repo-kind skills add steps: `write-go-service` adds the `write-openapi` step when a REST74contract changes; `write-go-kit` adds the public-API and docs-site obligations for a graduated75package.)7677---7879## Dependencies8081The standing goal across both orgs: **the fewest libraries doing the most.** Every dependency is82a maintenance liability, a supply-chain surface, and a constraint on future choices.8384- **Stdlib first.** If `net/http`, `encoding/json`, `context`, `errors`, `fmt`, `time`,85 `crypto/*`, `slices`, `maps`, `cmp`, etc. cover the need, use them. Never pull a package for86 what the standard library already does.87- **Reuse before adding.** Prefer a package already in `go.mod`. A second library that overlaps88 one already present is almost never worth it.89- **Mine what's already imported.** When implementing, don't re-research online practices — the90 planning phase (`plan-feature`) already did that. Do read the **documentation of the libraries91 already in `go.mod`** for the task at hand: a helper, option, or whole subsystem an imported92 dependency already provides is code you neither write nor maintain.93- **Vet candidates.** A new dependency must be: well-maintained (recent commits, responsive94 issue tracker, real test coverage); **owned by an organization, not a single personal95 account** — org ownership survives a maintainer losing interest; and **broad** — pick the96 library covering the most of the surrounding problem space, so one dependency replaces three.97 A narrow utility from an individual's account is the worst combination.98- **Ask first — always.** Introducing a package not already in `go.mod` requires explicit99 developer approval. No exceptions, not even a small utility. (`a-novel-kit` repos hold this bar100 even higher — see `write-go-kit`; `a-novel` services keep their `internal/lib/` as close to101 empty as possible.)102- **Remove, don't accumulate.** During maintenance, actively look for dependencies — and103 hand-rolled helpers — that a newer upstream now subsumes, and delete the duplicate.104105---106107## Package naming108109One directory = one package. Package names are **short, lowercase, single words** — no110underscores, no camelCase: `package dao`, `package jwk`, `package httpf`. The name is what every111caller types in imports; keep it unambiguous, collision-free, and non-stuttering with the types112inside it (`jwk.Key`, not `jwk.JwkKey`).113114---115116## File naming117118Multi-word file names are **camelCase** — never snake_case (`master_key.go` ✗), never119run-together when the name is two words (`masterkey.go` ✗): `masterKeyContext.go`,120`userSearch.go`. A test file mirrors the production file it covers with a `_test.go` suffix121(`userSearch.go` → `userSearch_test.go`) — see `write-go-tests` for why the underscore (not a122dot) matters and how to share fixtures across packages. The repo-kind skills define the123layer/role prefixes (`pg.*`, `rest.*`, `grpc.*`, `*.config.go`, `common.go`, …).124125---126127## Naming conventions128129### Variables and fields130131- **Be explicit.** `userDao`, not `repo`. `orderCreateService`, not `svc`. Length is not132 the cost; ambiguity is.133- **Short names are fine for conventional roles only**: `w` / `r` for HTTP handler params, `ctx`134 for `context.Context`, `err` for errors, `i` / `k` / `v` in range loops, `t` for `*testing.T`.135- **Acronyms keep ecosystem casing**: `ID`, `URL`, `JSON`, `JWT`, `JWK`, `HTTP`, `REST`, `gRPC`,136 `SQL`, `TTL`. An acronym that _starts_ an unexported identifier goes all-lowercase: `id`,137 `url`, `grpc`.138- **Don't shadow imported package names** (`json`, `http`, `context`, `errors`, `time`, …).139 Rename the variable.140141### Constructors142143Every type with a constructor uses `New<TypeName>(deps...) *<TypeName>`. Return the concrete144pointer, not an interface — unless the concrete type is genuinely an implementation detail hidden145by design (e.g. a `pkg/go` client behind an interface).146147**Always the composite-literal form**, never `new(T)`:148149```go150// WRONG — `new(T)` is a second style for the same thing; mixing the two is noise.151func NewPgUserSelect() *PgUserSelect { return new(PgUserSelect) }152153// CORRECT — reads identically whether the struct is empty or has fields.154func NewPgUserSelect() *PgUserSelect { return &PgUserSelect{} }155func NewUserSearch(r UserSearchDao) *UserSearch { return &UserSearch{dao: r} }156```157158Treat any `new(T)` you find as cleanup-on-sight when the file is already in scope.159160---161162## Error handling163164- **Lowercase, no trailing punctuation.** Errors get wrapped and read mid-sentence:165 `errors.New("user not found")`, not `"User not found."`.166- **Sentinels for expected outcomes**: `var ErrUserNotFound = errors.New("user not found")`,167 defined in the same file as the type that produces it (or a shared file if reused across the168 package). Map an upstream/library error onto your own sentinel by _joining_ it — `err =169errors.Join(err, ErrUserNotFound)` — so callers keep both identities.170- **One vocabulary per concept.** If the type is `Jwk`, every error message that refers to it171 says `"jwk …"` — never `"key not found"` in one file and `"jwk not found"` in another. Wording172 drift is easy to create and hard to dashboard around. Take the term from the type name and use173 it everywhere the error surfaces.174- **Wrap with context and `%w`**: `fmt.Errorf("search users: %w", err)`. Always `%w`, never175 `%v`, so callers keep `errors.Is` / `errors.As` identity. The message chain should let a reader176 trace the call path.177- **Never silently discard an error.** If one truly can be dropped, write `_ = ...` with a178 comment saying why.179180### Reporting errors on spans / telemetry — the layer-relative rule181182When a function instruments itself with a span (or any other per-operation telemetry), one rule183governs reporting: **every layer that has a span records, on its own span, every error it sees —184whether it raises it, propagates it, or maps it to a transport response.** Two moves are forbidden:185_suppressing_ reporting based on the error's _identity_ at a propagating layer, and a bare `return186nil, ErrXxx` from a layer that has a span. "Expected" is never a property the error value carries,187nor something one layer guesses on a caller's behalf.188189- A layer that _raises_ an error (a DAO hitting `sql.ErrNoRows`, a validator producing190 `ErrInvalidRequest`, a service detecting a mismatch) → `otel.ReportError(span, err)`.191- A layer that _receives_ an error and returns it upward → still `otel.ReportError`. Returning192 upward is _propagating_; wrapping it (`errors.Join`, `fmt.Errorf("...: %w", err)`) changes193 nothing.194- The handler that maps the error to a transport response → still reports. `golib/httpf.HandleError`195 calls `otel.ReportError` unconditionally before writing the HTTP status, so the REST handler span196 records the error whatever status it maps to; the gRPC manual mapping should do the same by hand197 (`_ = otel.ReportError(span, err)` before `status.Error(...)`). The handler span exists to show198 which error a request ended on.199- **Anti-pattern**: a helper that suppresses reporting based on the error's _identity_ at a layer200 that still propagates or surfaces it (a `reportUnexpected(span, err)` keyed on a list of "known"201 sentinels). It couples the layer to an error registry and silently drops real signal. The202 layer-local question is just "did I see this error?" — if yes, report it.203204Spans are independent — a child span ending `Error` does not taint the parent. So the DAO, service,205_and_ handler spans all say "no row" for a 404, while the "is the service broken" view is built on206the **HTTP status code** (recorded by the otel HTTP instrumentation), which counts a 404 as a 404.207Span status answers "did an error occur in processing", which is `true` even for a deliberate 404,208and that is fine. For bulk-anomaly visibility on a specific security sentinel, use a counter, an209audit log, or a dedicated event rather than `span.status`. The helpers210`otel.ReportError` / `otel.ReportSuccess` / `otel.ReportSuccessNoContent` live in `golib/otel`;211`ReportError` only sets `RecordError` + `SetStatus(Error)` — it does **not** end the span (a212`defer span.End()` does), and returning a sentinel with no `Report*` call leaves the span `Unset`,213which backends treat as "completed, not a failure". `write-go-service` covers span naming and the214span-per-operation rule.215216---217218## Context rules219220- **Never store `context.Context` in a struct.** It is request-scoped and must not outlive the221 call. Pass it as the first parameter of every method that needs it.222- **Contexts flow downward only** — caller to callee. Never return one.223- **Context values are for request-scoped data that crosses API boundaries** — DB transactions,224 trace spans, auth tokens. They are not a back door for optional arguments.225226---227228## Time capture229230When an operation derives more than one timestamp from "now" — `created_at` + `expires_at`,231`start` + `deadline`, any paired audit fields — capture `now := time.Now()` **once** and reuse it:232233```go234// WRONG — two wall-clock reads; expires_at - created_at is not exactly the TTL, and a test can't freeze it.235repo.Exec(ctx, &dao.InsertRequest{Now: time.Now(), Expiration: time.Now().Add(cfg.TTL)})236237// CORRECT — one logical instant.238now := time.Now()239repo.Exec(ctx, &dao.InsertRequest{Now: now, Expiration: now.Add(cfg.TTL)})240```241242Only call `time.Now()` more than once when you genuinely want distinct measurements (e.g.243computing an elapsed duration).244245---246247## Secrets and sensitive data248249- **Never log, trace, or put in span attributes**: passwords, tokens, API keys, private keys,250 key ciphertexts, signed JWTs, or any other credential material. Record identifiers only251 (`user.id`, `key.id`). A redacted `"*****"` of the same length still leaks the input length over252 every trace — don't do that either.253- **Never return secret material in an error message or a transport response.** Errors get254 logged; responses get cached and indexed.255- **Compare secrets in constant time** — `crypto/subtle.ConstantTimeCompare`, never `==` or256 `bytes.Equal` — and on the "not found" branch of an authentication path do the equivalent work257 (e.g. a throwaway hash comparison) so a lookup miss costs the same as a wrong-secret outcome258 and timing does not reveal whether the subject exists.259260---261262## Loop variable scope263264These codebases target Go 1.22+, where `for` loop variables are per-iteration; a closure captures265the right value with no extra copy. Remove any `current := item` shadow copy inside a `for range`266loop when you encounter one — it is dead code on this minimum version.267268---269270## Common pitfalls271272- **`context.Context` stored in a struct.** Always a method parameter.273- **A new dependency added without asking.** Explicit developer approval, every time.274- **Logging / tracing secret material.** Identifiers only — never the secret.275- **Multiple `time.Now()` for timestamps that should share one instant.** Capture once, reuse.276- **Suppressing span reporting for an error.** Every span'd layer reports every error it sees — no277 identity-keyed `reportUnexpected` helper, no bare `return nil, ErrXxx` from a layer with a span.278- **`new(T)` in a constructor.** Use `&T{}`.279- **snake_case or run-together multi-word file names.** camelCase.280- **Discarding an error without `_ =` and a why-comment.**281- **Shadowing an imported package name with a local variable.** Rename the variable.