Context values - Use context values only for request-scoped metadata that must cross API boundaries, not as a bag of optional parameters.
Coverage - Use coverage to find blind spots, not as a proxy for test quality; measure packages and integration paths deliberately.
Database timeouts - Database calls should usually be context-bound, cancellable, and aligned with request or job deadlines.
Defer - Use defer for cleanup at the point resources are acquired, but understand its execution cost and timing.
Dependency injection - Prefer explicit constructor wiring and small interfaces over framework-heavy dependency injection in Go.
Docs and comments - Go doc comments are part of the API surface and should explain intent, not restate code.
Embedding - Embed types to compose behavior or promote fields deliberately, not to simulate inheritance.
errgroup - Use `errgroup` when sibling goroutines should return the first error and share cancellation.
Error comparison - Compare errors with `errors.Is` and `errors.As` when wrapping may be involved; direct equality and brittle type checks fail easily.
Error handling - Treat errors as ordinary values, return them early, and add context without obscuring control flow.
Error wrapping - Wrap errors when extra context helps callers and keep the original cause inspectable with `%w`.
errors.Join - Use `errors.Join` when multiple sibling failures matter and callers still need `errors.Is` and `errors.As` to work.
Escape analysis - Let escape analysis inform allocation work, but do not contort APIs around compiler heuristics without measurement.
Execution tracing - Use execution tracing when latency depends on scheduler, blocking, GC, or network interactions that profiles alone cannot explain.
Functional options - Use functional options when a constructor has many optional settings and defaults should stay readable.
Fuzzing - Use fuzzing for parsers, decoders, and transformations where unexpected inputs matter more than enumerating all cases by hand.
Garbage collector - Understand allocation rate, live heap, and latency tradeoffs before tuning GC behavior.
Generics - Use generics when they simplify repeated type-safe logic; avoid them when interfaces or concrete code are clearer.
go and toolchain directives - Set `go` to the minimum language expectation and use `toolchain` only when developer ergonomics need a newer default toolchain.
go generate - Use `go generate` for explicit developer-invoked generation steps; keep outputs reproducible and reviewable.
Go workspaces - Use `go.work` for multi-module local development, not as the published dependency contract consumers rely on.
go:embed - Use `//go:embed` for small static assets shipped with the binary; keep patterns explicit and package-local.
Golden files and testdata - Use `testdata/` and golden files for stable external fixtures; make updates explicit and diffable.
Goroutine lifecycle - Every goroutine should have a clear owner, exit condition, and cancellation path.
govulncheck - Run `govulncheck` in module-aware repos and triage reachable findings, not just package presence.
gRPC - Use gRPC when strongly typed RPC contracts, streaming, or cross-service schema discipline clearly help.
HTTP clients - Reuse clients, configure timeouts deliberately, and bind requests to context.
HTTP servers - Use the standard `net/http` server lifecycle deliberately: timeouts, shutdown, and handler ownership matter more than routing sugar.
I/O readers and writers - Design streaming APIs around `io.Reader` and `io.Writer` when data should flow incrementally instead of materializing eagerly.
Init functions - Keep `init` rare, local, and boring; avoid hiding application behavior in package initialization.
Integer overflow - Assume integer arithmetic can overflow silently; guard sizes, counters, and bounds math deliberately.
Interface design - Keep interfaces small and define them where they are consumed.
Internal packages - Use `internal/` to enforce non-public package boundaries inside a repo when code should not be imported externally.
JSON handling - Use the standard library JSON tools carefully, validate boundaries, and avoid loose decoding by default.
Linters - Run automated analysis routinely; catching preventable mistakes early is cheaper than reviewing or debugging them later.
Logging with slog - Use `log/slog` for structured logs with stable keys, contextual attributes, and clear handler boundaries.
Maps - Use maps for keyed lookup and membership checks, but remember they are reference-like mutable state that need synchronization.
Memory model - Concurrent correctness depends on happens-before relationships, not intuition.
Minimal version selection - Understand that Go chooses the minimum required dependency versions in the graph, not the newest releases available.
Module compatibility - Prefer compatibility-preserving evolution and understand import versioning before breaking APIs.
Modules - Use modules as the unit of versioned distribution and route workspace, replace, private-module, and version-selection questions to narrower pages.
Named result parameters - Use named results when they genuinely clarify a small function or a defer-based cleanup path; otherwise keep returns explicit.
net/http - Prefer the standard library HTTP stack unless you have a concrete need not met by it.
Nil interfaces - An interface value is only nil when both its dynamic type and dynamic value are nil.
Nil vs empty slices - Choose `nil` versus empty slices deliberately at API boundaries because callers and encoders can observe the difference.
Package layout - Organize Go code by clear package boundaries, not framework-style folders.
Panic and recover - Reserve panic for unrecoverable invariants and internal programmer errors, not normal control flow.
Pipelines - Pipeline stages should close outbound channels, respect cancellation, and avoid blocked senders.
Private modules and proxies - Configure `GOPRIVATE` and related settings intentionally so private code stays private while public modules keep proxy and checksum protections.
Profiling - Measure first with pprof and tracing before changing code for performance.
Race detector - Use the race detector routinely in tests; data races are correctness bugs, not minor warnings.
Range loops - Remember that `range` copies element values and evaluates the range expression once before looping.
Receiver choice - Choose pointer or value receivers consistently based on mutation, size, and semantics.
Reflection - Use reflection sparingly at framework or boundary layers; prefer ordinary static Go code when you can.
Replace directives - Use `replace` for deliberate local development or controlled forks; keep it explicit and temporary when possible.
Select patterns - Use `select` to express multiplexed communication and cancellation clearly; avoid busy loops and fairness assumptions.
Slices - Treat slices as views over arrays with length, capacity, and aliasing behavior that matter in APIs.
SQL with database/sql - Use `database/sql` handles deliberately: context-aware queries, bounded lifetimes, and transaction scope that matches the unit of work.
Strings - Treat strings as immutable byte sequences that may contain UTF-8; choose byte- or rune-oriented operations deliberately.
Struct tags - Use struct tags as narrow metadata for boundary layers, not as a hidden control plane for core domain logic.
Subtests - Use subtests to structure related cases clearly and to isolate failures without duplicating test harness code.
sync.Cond - Use `sync.Cond` when goroutines wait on state transitions protected by a mutex, especially when channels fit poorly.
sync.Map - Use `sync.Map` for the narrow cases it was designed for, not as a default concurrent map replacement.
sync.Mutex - Use a mutex to protect shared mutable state when that is simpler than channel-based coordination.
sync.Once - Use `sync.Once` or `OnceValue` for one-time shared initialization; do not open-code racy lazy init.
sync.Pool - Use `sync.Pool` only for temporary reusable objects that may be dropped at any GC cycle.
sync.WaitGroup - Call `Add` before starting work, pair each increment with one `Done`, and keep ownership simple.
Table-driven tests - Use table-driven tests when many cases share the same structure and assertions.
Test execution modes - Use `go test` execution modes such as parallelism, shuffle, and short mode to expose hidden coupling and control scope.
Test helpers - Use `t.Helper` and small helper functions to remove repetition without hiding test intent.
Testing - Use deterministic tests with clear diagnostics, then route helpers, coverage, fixtures, and concurrency-specific questions to narrower pages.
testing/synctest - Use `testing/synctest` for deterministic tests of concurrent code that otherwise depend on timing or scheduler luck.
Tickers - Stop tickers you create, prefer timers for one-shot waits, and do not let periodic work outlive its owner.
Time durations - Express durations with explicit units or parsing; raw integers often become nanoseconds by accident.
time.After - Use `time.After` for simple one-shot waits, not hot loops where timer allocation and cleanup behavior matter.
Type assertions - Use type assertions when runtime type variation is real, and always handle the non-matching case deliberately.
unsafe - Use unsafe only when you can justify a measurable need and maintain strict invariants around memory assumptions.
Variable shadowing - Avoid reusing names across nested scopes when `:=` can silently change which value later code observes.
Vendoring - Vendor only when you need hermetic or policy-driven builds; treat `vendor/` as generated dependency snapshot, not hand-edited source.
Worker pools - Use worker pools when you need bounded concurrency, backpressure, and predictable lifecycle management.
Zero values - Design Go types so the zero value is useful when practical.