# goforagents.com Deep routing guide plus the full topics bundle. Preferred retrieval flow: 1. Fetch `/lookup.json` if you have a code question and need matching topic slugs. 2. Fetch `/topics//index.md` for narrow guidance. 3. Fetch `/bundles/.md` for group-scoped review. 4. Fetch `/all.md` only for broad review passes. Discovery surfaces: - `/index.md` root markdown entrypoint - `/topics/index.json` canonical topic catalog - `/search.json` compact topic metadata - `/sources.json` source provenance catalog Group bundles: - `API design and structure`: `/bundles/api-design-and-structure.md` - `Language fundamentals`: `/bundles/language-fundamentals.md` - `Concurrency`: `/bundles/concurrency.md` - `Errors and control flow`: `/bundles/errors-and-control-flow.md` - `Performance and runtime`: `/bundles/performance-and-runtime.md` - `I/O and systems`: `/bundles/io-and-systems.md` - `Testing`: `/bundles/testing.md` - `Modules and compatibility`: `/bundles/modules-and-compatibility.md` Bundle note: `/all.md` is currently about 126213 bytes and grows with corpus size. --- # Go For Agents Bundle Topics-only bundle for broad agent review passes. ### Dependency injection - Path: /topics/dependency-injection/index.md - Authority: primary - Last reviewed: 2026-03-26 - Check (should): dependencies are wired explicitly through constructors or plain structs - Check (prefer): DI containers are avoided unless they solve a real composition problem - Check (must): interfaces are introduced for behavior boundaries, not just for injection #### Canonical guidance - use plain constructors and explicit wiring first - inject behavior boundaries, not every concrete dependency - keep initialization visible in `main` or a small composition root #### Use when - wiring services and repositories - testing call boundaries - sharing infrastructure dependencies safely #### Avoid - framework-style containers as a default - interface-per-struct designs - hiding startup wiring behind reflection #### Preferred pattern ```go type Service struct { Repo UserRepo Log *slog.Logger } func NewService(repo UserRepo, log *slog.Logger) *Service { return &Service{Repo: repo, Log: log} } ``` #### Anti-pattern - adding a DI framework before simple constructor wiring becomes painful Explanation: This anti-pattern is tempting because containers promise flexibility, but in Go they often hide control flow without reducing real complexity. #### Why - explicit wiring keeps ownership and startup behavior easy to inspect --- ### Docs and comments - Path: /topics/docs-comments/index.md - Authority: primary - Last reviewed: 2026-03-26 - Check (should): doc comments explain behavior and contracts rather than paraphrasing code - Check (should): exported APIs that need explanation have clear identifier-led doc comments - Check (must): comments are kept in sync with behavior or removed #### Canonical guidance - Write package comments for packages users import directly. - Start doc comments with the declared identifier name. - Explain behavior, contracts, and caveats, not obvious syntax. - Keep comments current or delete them. #### Use when - documenting exported APIs - reviewing public package quality - preparing a library for outside use #### Avoid - comments that merely paraphrase code - stale comments that contradict behavior - documenting only what, never why or when #### Preferred pattern ```go // Fetch loads the user by ID and returns ErrNotFound when no row exists. func Fetch(ctx context.Context, id string) (User, error) ``` #### Anti-pattern ```go // Fetch fetches the user. func Fetch(ctx context.Context, id string) (User, error) ``` Explanation: This anti-pattern is tempting when documenting quickly, but comments that restate code add noise and go stale without helping callers. #### Why - in Go, documentation is tightly integrated with package browsing - good comments lower API misuse and reduce source-diving --- ### Functional options - Path: /topics/functional-options/index.md - Authority: community - Last reviewed: 2026-03-26 - Check (should): functional options are used when many optional parameters exist, not for every constructor by default - Check (must): the zero-configuration path remains obvious and valid - Check (should): option interactions are validated when the object is built #### Canonical guidance - prefer simple constructors until option count justifies more structure - use functional options when defaults matter and optional knobs keep growing - validate options in one place during construction #### Use when - clients with many optional settings - APIs that need forward-compatible configuration growth - constructors that would otherwise take many zero values #### Avoid - using functional options for one or two obvious parameters - mutating live objects through reused option functions - hiding required dependencies as optional knobs #### Preferred pattern ```go type Option func(*Client) func WithTimeout(d time.Duration) Option { return func(c *Client) { c.timeout = d } } func NewClient(addr string, opts ...Option) *Client { c := &Client{addr: addr, timeout: 5 * time.Second} for _, opt := range opts { opt(c) } return c } ``` #### Anti-pattern - constructors that require callers to pass long runs of zero values or `nil` placeholders Explanation: This anti-pattern is tempting because positional parameters are simple at first, but optional configuration scales poorly once defaults and growth matter. #### Why - functional options can preserve readable call sites while keeping defaults centralized --- ### Generics - Path: /topics/generics/index.md - Authority: primary - Last reviewed: 2026-03-26 - Check (should): generics are used only when they remove real repetitive type-safe logic - Check (should): interfaces or concrete code are preferred when they are simpler - Check (must): constraints remain readable and justified by the abstraction #### Canonical guidance - use generics for algorithms and data structures that genuinely repeat across types - prefer plain interfaces or concrete types when they are simpler - do not introduce type parameters purely for sophistication #### Use when - reusable containers - helpers that operate the same way across element types - removing real duplication without losing clarity #### Avoid - generic wrappers around single concrete use cases - replacing ordinary interface-based design with needless type parameters - unreadable constraints #### Preferred pattern ```go func Index[S ~[]E, E comparable](s S, v E) int { for i := range s { if s[i] == v { return i } } return -1 } ``` #### Anti-pattern - generic APIs whose only benefit is avoiding a handful of explicit overloads in application code Explanation: This anti-pattern is tempting after learning generics, but abstraction without repeated use makes APIs harder to read without reducing real duplication. #### Why - generics are a tool for clarity and reuse, not a new default for all abstractions --- ### go generate - Path: /topics/go-generate/index.md - Authority: primary - Last reviewed: 2026-03-26 - Check (must): `go generate` is an explicit development step, not a hidden dependency of ordinary builds - Check (should): generation commands are deterministic enough for reviewed output and CI reproduction - Check (should): generated files are clearly marked and kept out of normal hand-edit workflows #### Canonical guidance - use `go generate` for explicit source generation - keep generator commands reproducible and documented - decide deliberately whether generated output is committed #### Use when - stringers - mocks or stubs when the repo policy allows them - code derived from schemas or protocol definitions #### Avoid - hiding generation inside ordinary `go build` - undocumented external generator dependencies - hand-editing generated files #### Preferred pattern ```go //go:generate stringer -type=State type State int ``` #### Anti-pattern - requiring `go generate` to make the package compile, but not documenting or enforcing it anywhere Explanation: This anti-pattern is tempting because it keeps the build "automatic" locally, but it fails unpredictably for everyone else. #### Why - explicit generation is manageable; hidden generation is operational debt --- ### Init functions - Path: /topics/init-functions/index.md - Authority: mixed - Last reviewed: 2026-03-26 - Check (must): `init` only performs small deterministic setup that truly belongs to package initialization - Check (should): `init` does not perform network calls, start goroutines, or hide application wiring - Check (prefer): explicit constructors and startup functions are preferred over hidden package initialization #### Canonical guidance - use `init` sparingly - keep initialization deterministic and cheap - prefer explicit constructors and startup paths for real application logic #### Use when - package-local registration - validating static invariants - computing small derived defaults #### Avoid - reading environment and configuring the whole app in `init` - doing I/O in `init` - relying on import side effects for ordinary behavior #### Preferred pattern ```go var defaultTimeout = 5 * time.Second func init() { if defaultTimeout <= 0 { panic("invalid default timeout") } } ``` #### Anti-pattern - packages that open connections or launch background work as soon as they are imported Explanation: This anti-pattern is tempting because `init` runs automatically, but hidden startup logic makes order, testing, and configuration harder to reason about. #### Why - explicit setup is easier to test, override, and debug --- ### Interface design - Path: /topics/interface-design/index.md - Authority: primary - Last reviewed: 2026-03-26 - Check (should): interfaces remain small and behavior-focused - Check (should): interfaces are defined where they are consumed when practical - Check (must): concrete types are preferred until abstraction is actually needed #### Canonical guidance - prefer concrete types until abstraction is needed - define interfaces at the point of use, usually on the consumer side - keep interfaces small, often one or a few methods - use interfaces to describe behavior, not taxonomy #### Use when - mocking or testing call boundaries - abstracting over multiple implementations - reviewing exported APIs #### Avoid - exporting interfaces just because there is one implementation - giant “god interfaces” - adding an interface layer before a second implementation exists #### Preferred pattern ```go type Fetcher interface { Fetch(ctx context.Context, id string) (User, error) } ``` #### Anti-pattern ```go type ServiceManagerInterface interface { Start() error Stop() error Fetch(ctx context.Context, id string) (User, error) Save(ctx context.Context, u User) error Delete(ctx context.Context, id string) error } ``` Explanation: This anti-pattern is common in enterprise-style codebases, but oversized interfaces freeze unnecessary abstraction into APIs too early. #### Why - small interfaces are easier to satisfy and reason about - consumer-owned interfaces avoid over-design in providers --- ### Internal packages - Path: /topics/internal-packages/index.md - Authority: primary - Last reviewed: 2026-03-26 - Check (must): `internal/` is used only for code that should be inaccessible outside its allowed subtree - Check (should): internal package boundaries reflect real ownership or API boundaries rather than arbitrary nesting - Check (prefer): `internal/` is not used as a generic bucket for unrelated helpers #### Canonical guidance - use `internal/` when the compiler should enforce that a package is not public API - keep package boundaries meaningful; `internal/` does not replace good package design - prefer a few coherent internal packages over many tiny hidden ones #### Use when - shared code for multiple commands in one repo - unstable implementation details - non-public adapters or wiring packages #### Avoid - putting broadly reusable libraries under `internal/` - adding `internal/` just to hide unclear architecture - scattering one-off helper packages everywhere #### Preferred pattern ```text repo/ internal/ authz/ config/ cmd/api/ cmd/worker/ ``` #### Anti-pattern - copying the same code into several public packages because a useful shared package was hidden under the wrong `internal/` boundary Explanation: This anti-pattern is tempting because hiding code feels safer, but a bad boundary creates duplication instead of clarity. #### Why - `internal/` gives compiler-enforced API boundaries inside a repository --- ### Linters - Path: /topics/linters/index.md - Authority: mixed - Last reviewed: 2026-03-26 - Check (must): `go vet` or an equivalent baseline analysis runs regularly in local or CI workflows - Check (should): the lint set is curated for useful signal instead of becoming ignored noise - Check (prefer): recurring findings become coding standards or helper abstractions rather than recurring review comments #### Canonical guidance - run `go vet` by default - add extra linters only if the team will keep them actionable - treat repeated lint findings as process or design problems #### Use when - CI pipelines - pre-merge checks - large codebases with many contributors #### Avoid - disabling findings without understanding them - running a huge linter bundle nobody respects - treating lint as style-only busywork #### Preferred pattern ```bash go vet ./... go test ./... ``` #### Anti-pattern - relying on code review alone to catch shadowing, suspicious formatting directives, or obvious misuse patterns Explanation: This anti-pattern is tempting because review feels sufficient, but machines are better at repetitive detection and humans are better at design judgment. #### Why - static analysis finds low-level mistakes early and consistently --- ### Package layout - Path: /topics/package-layout/index.md - Authority: primary - Last reviewed: 2026-03-26 - Check (should): packages are split by cohesive responsibility rather than file type taxonomies - Check (prefer): the repo is not over-structured before the code demands it - Check (should): internal boundaries are used deliberately for non-public implementation detail #### Canonical guidance - Start simple; do not invent a deep layout before the code demands it. - Split packages by cohesive responsibility, not by type of file. - Keep `internal/` for implementation details not meant for external import. - Prefer packages that expose small, clear APIs. #### Use when - starting a new service or library - extracting subpackages - reviewing whether a repo is over-structured #### Avoid - Java-style folder taxonomies for their own sake - `models`, `helpers`, `services`, `managers` packages with mixed responsibilities - premature multi-package decomposition #### Preferred pattern ```go package httpapi import "example.com/acme/inventory/internal/store" type Server struct { Store *store.Store } ``` #### Anti-pattern ```text /controllers /services /repositories /helpers /utils ``` Explanation: This anti-pattern is tempting because framework taxonomies feel organized, but they usually increase indirection without improving Go package boundaries. #### Why - Go packages are architectural boundaries - shallow, responsibility-driven packages make imports and ownership clearer - over-structured repos create indirection without improving design --- ### Package names - Path: /topics/package-names/index.md - Authority: primary - Last reviewed: 2026-03-26 - Check (should): package names stay short and lower-case - Check (must): package and identifier combinations avoid stutter - Check (should): generic catch-all package names like util or common are avoided #### Canonical guidance - Prefer short, lower-case package names. - Let the import path provide context; do not repeat it in identifiers. - Avoid generic names like `util`, `common`, or `base` unless the package really is that. - Optimize for call-site readability. #### Use when - naming any new package - reviewing public APIs - splitting a large package into smaller ones #### Avoid - `package utils` - stutter like `http.HTTPServer` - cute abbreviations that are not standard to Go users #### Preferred pattern ```go package httpapi ``` ```go client := httpapi.NewClient() ``` #### Anti-pattern ```go package commonutils ``` ```go client := commonutils.NewHTTPAPIClient() ``` Explanation: This anti-pattern is common because generic names feel reusable, but they make call sites vague and encourage dumping unrelated behavior together. #### Why - package names are read constantly at import sites and call sites - shorter names reduce stutter and improve scan speed - the best package names usually make extra explanation unnecessary --- ### Receiver choice - Path: /topics/receiver-choice/index.md - Authority: primary - Last reviewed: 2026-03-26 - Check (should): receiver choice is consistent for a type - Check (must): pointer receivers are used when methods mutate state or copying is undesirable - Check (must): types containing mutexes are not exposed through unsafe value-copy patterns #### Canonical guidance - use pointer receivers when methods mutate state - use pointer receivers for large structs or when copying is undesirable - avoid mixing pointer and value receivers on the same type unless there is a strong reason - value receivers fit small immutable value-like types #### Use when - designing methods for a new type - reviewing API consistency #### Avoid - mixed receiver sets on ordinary structs - value receivers on types that contain mutexes - pointer receivers just by habit when value semantics are intended #### Preferred pattern ```go type Counter struct { n int } func (c *Counter) Inc() { c.n++ } ``` #### Anti-pattern ```go func (c Counter) Inc() { c.n++ } ``` Explanation: This anti-pattern is tempting when methods evolve one by one, but mixed receiver semantics create subtle copying and API-consistency issues. #### Why - receiver choice communicates semantics - mixed choices create confusion around copying and mutation --- ### Variable shadowing - Path: /topics/variable-shadowing/index.md - Authority: mixed - Last reviewed: 2026-03-26 - Check (should): important values such as `err` are not rebound in inner scopes unless the tradeoff is obvious - Check (must): plain assignment is used when updating an existing variable instead of accidentally creating a new one - Check (prefer): scopes stay small enough that name reuse does not hide control flow #### Canonical guidance - keep scopes narrow and names distinct - be cautious with `:=` when an outer variable already exists - prefer `=` when reusing an existing binding is intended #### Use when - error-heavy control flow - nested `if` blocks - loops with temporary variables #### Avoid - shadowing `err` and then reading the outer `err` later - reusing short names across distant nested scopes - large functions where scope boundaries are hard to see #### Preferred pattern ```go f, err := os.Open(name) if err != nil { return err } defer f.Close() _, err = io.Copy(dst, f) if err != nil { return err } ``` #### Anti-pattern - mixing outer and inner `err` variables so later checks inspect the wrong one Explanation: This anti-pattern is tempting because short declarations are convenient, but one extra `:=` can quietly change which variable later code sees. #### Why - shadowing creates bugs that still compile and often still look reasonable in review --- ### Zero values - Path: /topics/zero-values/index.md - Authority: primary - Last reviewed: 2026-03-26 - Check (should): types are designed so the zero value is useful when practical - Check (prefer): constructors are used only when zero value cannot express a valid state - Check (must): types with invalid zero values document that constraint clearly #### Canonical guidance - prefer types whose zero value is ready for basic use - make configuration additive instead of requiring a constructor for every case - document when the zero value is not valid #### Use when - designing structs and APIs - deciding whether a constructor is truly necessary #### Avoid - types that panic when zero-initialized unless unavoidable - needless constructors that only assign zero values #### Preferred pattern ```go var buf bytes.Buffer buf.WriteString("ok") ``` #### Anti-pattern ```go type Client struct { conn *Conn } ``` - if zero `Client` is invalid, document it clearly or redesign the type Explanation: This anti-pattern is tempting when constructors are familiar from other languages, but unnecessary ceremony fights Go’s zero-value conventions. #### Why - useful zero values reduce ceremony - they fit Go’s allocation and declaration style naturally --- ### Atomics - Path: /topics/atomics/index.md - Authority: primary - Last reviewed: 2026-03-26 - Check (should): atomics are limited to narrow synchronization cases with a precise visibility story - Check (prefer): mutexes are preferred over atomics unless a simple atomic primitive clearly suffices - Check (must): multiple atomics are not used to simulate a larger unsynchronized transaction #### Canonical guidance - prefer mutexes unless a simple atomic primitive clearly suffices - use atomics for narrow cases like flags, counters, and pointers with precise semantics - reason in terms of synchronization and visibility, not speed folklore #### Use when - single-value shared state - fast-path flags - lock-free structures only with strong expertise and tests #### Avoid - combining many atomics into a pseudo-transaction - using atomics without understanding the required ordering - assuming atomics automatically simplify code #### Preferred pattern ```go type Gate struct { ready atomic.Bool } func (g *Gate) MarkReady() { g.ready.Store(true) } func (g *Gate) Ready() bool { return g.ready.Load() } ``` #### Anti-pattern - large state machines encoded through scattered atomic loads and stores Explanation: This anti-pattern is tempting because atomics look cheaper than locks, but scattered loads and stores usually hide correctness bugs behind superficial performance intuition. #### Why - atomics trade simplicity for lower-level control - misuse creates subtle concurrency bugs --- ### Buffered channels - Path: /topics/buffered-channels/index.md - Authority: primary - Last reviewed: 2026-03-26 - Check (should): channel capacity is justified by queueing, batching, or semaphore semantics rather than guesswork - Check (must): buffering is not used to mask unclear ownership, missing receivers, or shutdown bugs - Check (prefer): the code's intended backpressure behavior is still understandable after buffering is introduced #### Canonical guidance - use buffering when capacity has real semantics - keep backpressure behavior understandable - prefer unbuffered channels when synchronization is the point #### Use when - bounded work queues - semaphore-like throttling - absorbing short producer-consumer bursts #### Avoid - choosing capacity by superstition - increasing the buffer until flaky tests stop failing - assuming buffering makes leaks or deadlocks impossible #### Preferred pattern ```go sem := make(chan struct{}, 8) sem <- struct{}{} ``` #### Anti-pattern - adding a buffer just because the sender should "probably not block" Explanation: This anti-pattern is tempting because blocking feels scary, but unexplained buffering often hides a design bug instead of fixing it. #### Why - buffer size is part of the concurrency design, not a cosmetic tuning knob --- ### Cancellation - Path: /topics/cancellation/index.md - Authority: primary - Last reviewed: 2026-03-26 - Check (must): cancellation intent flows from the caller through context - Check (must): long-running work stops promptly when ctx.Done is closed - Check (should): spawned work and resources are cleaned up when cancellation occurs #### Canonical guidance - caller owns cancellation intent - pass cancellation through `context.Context` - stop goroutines promptly when `ctx.Done()` closes - clean up timers, channels, and spawned work #### Use when - request handlers - background operations with deadlines - pipelines and fan-out work #### Avoid - detached goroutines with no stop condition - custom cancellation channels when `context` already fits - ignoring `ctx.Err()` in long-running loops #### Preferred pattern ```go for { select { case <-ctx.Done(): return ctx.Err() case item := <-in: _ = item } } ``` #### Anti-pattern ```go go func() { for item := range in { process(item) } }() ``` - if nothing can stop this loop early, it can leak work Explanation: This anti-pattern is common in rushed concurrency code because the goroutine appears independent, but without cancellation it can outlive the caller and leak work. #### Why - cancellation is a resource-management concern, not just a timeout feature --- ### Channel closing - Path: /topics/channel-closing/index.md - Authority: primary - Last reviewed: 2026-03-26 - Check (must): a channel has one clear sending owner responsible for closing it - Check (must): `close` is used only to signal that no further values will be sent - Check (should): receivers use `range` or the receive `ok` result when closed-channel semantics matter #### Canonical guidance - the sending side closes when it knows no more values will arrive - receivers do not close channels they do not own - close channels for completion signals, not acknowledgments #### Use when - producer completion - pipeline shutdown - fan-out workers consuming a finite stream #### Avoid - multiple senders racing to close the same channel - closing from the receive side - treating `close` as a generic broadcast for unrelated state changes #### Preferred pattern ```go out := make(chan Item) go func() { defer close(out) for _, item := range items { out <- item } }() ``` #### Anti-pattern - adding `recover` around `close(ch)` because ownership is unclear Explanation: This anti-pattern is tempting because it suppresses panics, but it hides a lifecycle bug instead of fixing it. #### Why - channel shutdown is simple only when channel ownership is simple --- ### Channels - Path: /topics/channels/index.md - Authority: primary - Last reviewed: 2026-03-26 - Check (should): channels are used for communication or coordination rather than as decoration around shared state - Check (must): channels are closed only by the sending side when ownership is clear - Check (should): buffer sizes are chosen deliberately instead of hiding control-flow bugs #### Canonical guidance - use channels for communication and coordination between goroutines - prefer simple ownership transfer over shared mutable state - choose buffering deliberately; do not use it to hide design problems - close channels from the sending side when no more values will arrive #### Use when - pipelines - worker coordination - signaling completion or cancellation #### Avoid - using channels when a mutex around shared state is simpler - closing channels from the receiver side - multiple senders racing to close the same channel #### Preferred pattern ```go out := make(chan Item) go func() { defer close(out) for _, item := range items { out <- item } }() ``` #### Anti-pattern - channel graphs so complex that ownership and shutdown become unclear Explanation: This anti-pattern is tempting because channels feel idiomatic, but overly complex channel graphs often hide ownership and shutdown mistakes. #### Why - channels are excellent when they model dataflow or coordination - they are poor when used as decorative complexity around ordinary state access --- ### Context propagation - Path: /topics/context-propagation/index.md - Authority: primary - Last reviewed: 2026-03-26 - Check (must): context.Context is passed explicitly as the first parameter - Check (must): context is not stored in long-lived structs or globals - Check (should): child contexts are derived near the operation boundary #### Canonical guidance - Pass `context.Context` explicitly as the first parameter. - Derive child contexts near the operation boundary. - Cancel work you start when the work no longer matters. - Do not hide context in globals or long-lived structs. #### Use when - request-scoped work - cancellation - deadlines - tracing / request metadata #### Avoid - storing context on a service struct - passing `nil` context - using context for optional function arguments #### Preferred pattern ```go func (s *Service) Fetch(ctx context.Context, id string) error { ctx, cancel := context.WithTimeout(ctx, 2*time.Second) defer cancel() return s.repo.Fetch(ctx, id) } ``` #### Anti-pattern ```go type Service struct { ctx context.Context } ``` Explanation: This anti-pattern is common because storing context looks convenient, but it obscures request lifetime, cancellation ownership, and testability. #### Why - explicit context propagation makes cancellation and deadlines composable - hidden context makes lifetime and ownership unclear --- ### Context values - Path: /topics/context-values/index.md - Authority: primary - Last reviewed: 2026-03-26 - Check (must): context values carry request-scoped metadata rather than general configuration or business inputs - Check (must): context keys use typed, package-local identifiers to avoid collisions - Check (should): ordinary function inputs are passed explicitly instead of being hidden in context values #### Canonical guidance - use context values for metadata that must cross API boundaries - keep keys typed and package-local - pass real business inputs as explicit parameters #### Use when - request IDs - auth or tracing metadata - APIs that already accept context and need request-scoped values #### Avoid - storing configs or dependencies in context - using strings as public key types - hiding optional parameters in context because the function signature is inconvenient #### Preferred pattern ```go type requestIDKey struct{} func WithRequestID(ctx context.Context, id string) context.Context { return context.WithValue(ctx, requestIDKey{}, id) } ``` #### Anti-pattern - using `context.WithValue` to smuggle feature flags, database handles, or logger dependencies through the call graph Explanation: This anti-pattern is tempting because it avoids changing signatures, but it destroys API clarity and type safety. #### Why - context values work best for narrow cross-cutting metadata, not for ordinary program state --- ### errgroup - Path: /topics/errgroup/index.md - Authority: mixed - Last reviewed: 2026-03-26 - Check (should): `errgroup` is used when parallel work needs first-error propagation and shared cancellation - Check (must): goroutines launched under an `errgroup` use the derived context for cancelable downstream work - Check (prefer): manual waitgroup-plus-error-channel plumbing is avoided when `errgroup` fits the job #### Canonical guidance - prefer `errgroup` for sibling tasks that can fail independently - derive a shared context with `errgroup.WithContext` - return errors directly from worker functions #### Use when - parallel fetches - fan-out request handling - bounded multi-step workflows #### Avoid - using `WaitGroup` plus custom error channels for the same pattern - ignoring the derived context inside worker calls - forcing `errgroup` into fire-and-forget work #### Preferred pattern ```go g, ctx := errgroup.WithContext(ctx) for _, id := range ids { g.Go(func() error { return fetchOne(ctx, id) }) } if err := g.Wait(); err != nil { return err } ``` #### Anti-pattern - parallel code that has three separate mechanisms for waiting, canceling, and collecting errors Explanation: This anti-pattern is tempting because each piece seems simple, but the combination quickly becomes lifecycle glue the standard helper already solved. #### Why - `errgroup` compresses a common concurrency pattern into one readable abstraction --- ### Goroutine lifecycle - Path: /topics/goroutine-lifecycle/index.md - Authority: primary - Last reviewed: 2026-03-26 - Check (must): each goroutine has a clear owner - Check (must): each long-lived goroutine has an explicit exit condition or cancellation path - Check (should): completion is waited on or otherwise observed when relevant #### Canonical guidance - know who starts a goroutine - know what event makes it stop - know who waits for it or otherwise observes completion - tie long-lived goroutines to process or component lifecycle explicitly #### Use when - launching background work - designing worker pools - reviewing shutdown behavior #### Avoid - fire-and-forget goroutines with hidden side effects - goroutines that only terminate on process exit - no ownership model for background loops #### Preferred pattern ```go g, ctx := errgroup.WithContext(ctx) g.Go(func() error { return worker(ctx) }) return g.Wait() ``` #### Anti-pattern ```go go syncAllCaches() ``` Explanation: This anti-pattern is common because fire-and-forget feels simple, but unowned goroutines become leak, shutdown, and observability problems. #### Why - unmanaged goroutines are a common source of leaks, races, and shutdown bugs --- ### Memory model - Path: /topics/memory-model/index.md - Authority: primary - Last reviewed: 2026-03-26 - Check (must): shared concurrent state is protected by synchronization that establishes ordering - Check (must): concurrent correctness does not rely on assumed eventual visibility - Check (should): atomics are used only with a clear memory-model explanation #### Canonical guidance - if data is accessed concurrently, synchronization must establish ordering - channel operations, mutexes, and atomic operations matter because they create happens-before edges - absence of crashes does not prove correctness #### Use when - shared-memory concurrency - lock-free or atomic code - explaining visibility bugs #### Avoid - assuming writes become visible “soon enough” - hand-wavy reasoning about goroutine interleavings - writing concurrent code without understanding which operation synchronizes with which #### Preferred pattern ```go type Config struct { Addr string } func LoadConfig() Config { var cfg Config ready := make(chan struct{}) go func() { cfg = loadConfig() close(ready) }() <-ready return cfg } ``` #### Anti-pattern - sharing mutable state across goroutines with no synchronization because tests happened to pass Explanation: This anti-pattern is common when tests seem stable, but visibility bugs can stay hidden until production interleavings expose them. #### Why - concurrency bugs are often visibility and ordering bugs, not only race-detector bugs --- ### Pipelines - Path: /topics/pipelines/index.md - Authority: primary - Last reviewed: 2026-03-26 - Check (must): pipeline stages close outbound channels when done sending - Check (must): pipeline stages respect cancellation and do not strand senders - Check (should): buffering is not used as a substitute for lifecycle design #### Canonical guidance - each stage receives values, transforms them, and emits downstream - stages should close outbound channels when done sending - downstream abandonment must not strand upstream goroutines - cancellation must be designed into the pipeline #### Use when - streaming multi-stage work - fan-out / fan-in designs - CPU or I/O pipelines #### Avoid - pipeline stages with no cancellation story - leaving senders blocked forever - unbounded buffering as a substitute for control flow #### Preferred pattern ```go func sq(ctx context.Context, in <-chan int) <-chan int { out := make(chan int) go func() { defer close(out) for n := range in { select { case <-ctx.Done(): return case out <- n * n: } } }() return out } ``` #### Anti-pattern - stages that assume the downstream consumer will always drain all results Explanation: This anti-pattern is common because downstream consumers often drain everything in demos, but real callers frequently stop early and expose blocked senders. #### Why - pipeline bugs are often goroutine-leak bugs in disguise --- ### Race detector - Path: /topics/race-detector/index.md - Authority: primary - Last reviewed: 2026-03-26 - Check (should): concurrent code is exercised under go test -race or equivalent coverage - Check (must): reported data races are treated as correctness bugs - Check (must): sleeps are not used to paper over synchronization bugs #### Canonical guidance - run race detection in tests and representative concurrent workloads - treat reported races as real bugs - fix shared-memory ownership, synchronization, or lifecycle errors directly #### Use when - testing concurrent code - debugging flaky integration tests - reviewing code with shared state #### Avoid - dismissing races because code “usually works” - adding sleeps to hide synchronization bugs - assuming channels alone make all shared state safe #### Preferred pattern ```go func TestCounter(t *testing.T) { var mu sync.Mutex var n int var wg sync.WaitGroup for i := 0; i < 4; i++ { wg.Add(1) go func() { defer wg.Done() mu.Lock() n++ mu.Unlock() }() } wg.Wait() if n != 4 { t.Fatalf("n = %d, want 4", n) } } ``` #### Anti-pattern - merging concurrency-heavy code without race-detector coverage Explanation: This anti-pattern is common under schedule pressure, but timing hacks make tests pass while the underlying synchronization bug remains. #### Why - data races can corrupt behavior silently and nondeterministically --- ### Select patterns - Path: /topics/select-patterns/index.md - Authority: primary - Last reviewed: 2026-03-26 - Check (must): a `default` case does not create a hot busy loop unless that behavior is intentional and bounded - Check (should): long-lived blocking selects include cancellation or shutdown handling - Check (prefer): code does not rely on `select` choosing ready cases in a deterministic order #### Canonical guidance - keep each `select` arm meaningful and bounded - include cancellation paths in long-lived loops - do not assume deterministic fairness across ready cases #### Use when - cancellation-aware workers - multiplexing multiple channels - timeouts and shutdown signals #### Avoid - `default` branches that spin and burn CPU - giant `select` blocks mixing unrelated concerns - assuming one ready case always wins first #### Preferred pattern ```go for { select { case <-ctx.Done(): return ctx.Err() case item, ok := <-in: if !ok { return nil } handle(item) } } ``` #### Anti-pattern - adding a `default` just to avoid blocking when the caller actually wanted backpressure Explanation: This anti-pattern is tempting because non-blocking code feels safer, but it often turns coordination into dropped work or CPU spin. #### Why - `select` is precise when it models real coordination, not when it is used to dodge blocking semantics --- ### sync.Cond - Path: /topics/sync-cond/index.md - Authority: mixed - Last reviewed: 2026-03-26 - Check (should): `sync.Cond` is used for waiting on shared state transitions, not as a drop-in replacement for every channel - Check (must): `Wait` is always called in a loop that rechecks the guarded condition - Check (must): the same mutex consistently protects the condition being awaited #### Canonical guidance - use `sync.Cond` for condition waiting on shared state - always wait in a loop - hold the mutex while checking and updating the condition #### Use when - one state change wakes many waiters - polling would be wasteful - channels would create awkward ownership or fan-out #### Avoid - calling `Wait` without a loop - using `sync.Cond` when a simple channel close would do - guarding the condition with different locks #### Preferred pattern ```go mu := sync.Mutex{} cond := sync.NewCond(&mu) ready := false mu.Lock() for !ready { cond.Wait() } mu.Unlock() ``` #### Anti-pattern - treating `Signal` as if it permanently makes the condition true Explanation: This anti-pattern is tempting because wake-ups feel definitive, but the real contract is the shared condition, not the wake-up event itself. #### Why - `sync.Cond` is low-level but sometimes the cleanest fit for shared-state coordination --- ### sync.Map - Path: /topics/sync-map/index.md - Authority: primary - Last reviewed: 2026-03-26 - Check (must): `sync.Map` is used for read-mostly or disjoint-key concurrent patterns where it materially fits better than `map` plus lock - Check (should): type assertions around `sync.Map` are contained so untyped storage does not leak through the codebase - Check (prefer): ordinary `map` plus explicit locking remains the default when access patterns are simple #### Canonical guidance - prefer `map` plus mutex by default - use `sync.Map` for its intended concurrent access patterns - keep the untyped boundary small #### Use when - read-mostly caches - keys written once then read many times - concurrent access patterns with disjoint key sets #### Avoid - reaching for `sync.Map` as the default concurrent map - spreading `any` type assertions everywhere - assuming `sync.Map` is automatically faster #### Preferred pattern ```go var cache sync.Map cache.Store(key, value) v, ok := cache.Load(key) ``` #### Anti-pattern - replacing a small, easy-to-lock map with `sync.Map` because "concurrent data needs a concurrent container" Explanation: This anti-pattern is tempting because the API looks purpose-built, but it often makes simple state harder to understand and type-check. #### Why - `sync.Map` is specialized, not a general replacement for ordinary map ownership --- ### sync.Mutex - Path: /topics/sync-mutex/index.md - Authority: primary - Last reviewed: 2026-03-26 - Check (must): shared mutable state is protected by a clear mutex boundary - Check (should): critical sections stay small and obvious - Check (must): types containing a mutex are not copied after use begins #### Canonical guidance - use a mutex when multiple goroutines coordinate around shared mutable state - keep critical sections small and obvious - treat lock ownership and protected invariants as part of the design - prefer ordinary mutexes before reaching for more complex lock-free patterns #### Use when - maps or structs mutated by multiple goroutines - caches - shared counters with richer invariants than a single number #### Avoid - copying structs that contain a mutex after use begins - exporting unlocked internal state casually - replacing a straightforward mutex with channels just for style #### Preferred pattern ```go type Cache struct { mu sync.Mutex m map[string]string } func (c *Cache) Set(k, v string) { c.mu.Lock() defer c.mu.Unlock() c.m[k] = v } ``` #### Anti-pattern - protecting one invariant with several unrelated locks and unclear rules Explanation: This anti-pattern is tempting in growing codebases, but unclear lock boundaries make deadlocks and invariant violations much harder to reason about. #### Why - mutexes are often the clearest concurrency primitive for shared state --- ### sync.Once - Path: /topics/sync-once/index.md - Authority: primary - Last reviewed: 2026-03-26 - Check (must): shared one-time initialization uses `sync.Once` or the `OnceValue` helpers instead of ad hoc double-checked logic - Check (must): the one-time initialization function can run exactly once without relying on retries or partial reset - Check (should): code does not assume `sync.Once` can be reset for another lifecycle #### Canonical guidance - use `sync.Once` for shared one-time initialization - prefer `OnceValue` or `OnceValues` when the result should be returned cleanly - keep the initialization path simple and idempotent enough for one execution #### Use when - lazy singletons - cached expensive setup - shared initialization across many goroutines #### Avoid - hand-rolled boolean-plus-mutex init guards - assuming `sync.Once` supports reset - using once when eager package init would be simpler #### Preferred pattern ```go var loadConfig = sync.OnceValue(func() Config { return readConfig() }) ``` #### Anti-pattern - checking `if x == nil` without synchronization and then initializing from several goroutines Explanation: This anti-pattern is tempting because it looks cheap, but it is exactly the kind of publication bug `sync.Once` exists to avoid. #### Why - once-based initialization is clearer and safer than open-coded lazy init races --- ### sync.Pool - Path: /topics/sync-pool/index.md - Authority: primary - Last reviewed: 2026-03-26 - Check (must): pooled objects are treated as optional reuse and not as correctness-critical storage - Check (must): objects returned from the pool are reset before reuse when stale state would matter - Check (should): pooling is justified by measured allocation or GC pressure rather than assumption #### Canonical guidance - treat `sync.Pool` as an optimization only - assume pooled objects can disappear at any GC cycle - reset reused objects before handing them back out #### Use when - temporary buffers - short-lived reusable objects under measurable allocation pressure - high-throughput paths that profiles say allocate too much #### Avoid - storing long-lived state in the pool - assuming the pool is a cache with retention guarantees - introducing pools before measuring allocation costs #### Preferred pattern ```go var bufPool = sync.Pool{ New: func() any { return new(bytes.Buffer) }, } ``` #### Anti-pattern - relying on `sync.Pool` to keep expensive objects alive for later correctness-critical reuse Explanation: This anti-pattern is tempting because the API says "pool", but the runtime may drop entries whenever it likes. #### Why - pooling helps only when the runtime and workload make reuse likely; it is never a correctness primitive --- ### sync.WaitGroup - Path: /topics/sync-waitgroup/index.md - Authority: mixed - Last reviewed: 2026-03-26 - Check (must): `WaitGroup.Add` happens before the goroutine or task starts - Check (must): every added unit of work calls `Done` exactly once - Check (should): the code keeps clear ownership of who adds work and who waits for completion #### Canonical guidance - call `Add` before `go` - defer `Done` at the start of the goroutine - keep one clear owner for launch and one clear wait point - on newer Go versions, consider `WaitGroup.Go` when it makes launch ownership simpler #### Use when - waiting for sibling goroutines - bounded background work - simple fan-out without error propagation #### Avoid - calling `Add` inside the goroutine - copying a `WaitGroup` - mixing it with ad hoc shutdown rules nobody owns #### Preferred pattern ```go var wg sync.WaitGroup for _, job := range jobs { wg.Add(1) go func(job Job) { defer wg.Done() process(job) }(job) } wg.Wait() ``` #### Anti-pattern - racing `Wait` against `Add` because work registration happens after launch Explanation: This anti-pattern is tempting because the goroutine already "knows" it exists, but `WaitGroup` correctness depends on registration happening first. #### Why - `WaitGroup` is simple only when lifecycle ownership stays simple --- ### Worker pools - Path: /topics/worker-pools/index.md - Authority: primary - Last reviewed: 2026-03-26 - Check (must): worker pools are used to enforce bounded concurrency when load can spike - Check (must): worker pools have explicit cancellation and shutdown behavior - Check (should): queue growth and backpressure policy are explicit #### Canonical guidance - use worker pools to cap concurrency, not as a default pattern for all parallel work - make queueing, cancellation, and shutdown explicit - prefer simple worker loops over elaborate pool frameworks #### Use when - bounded background processing - controlled fan-out over many jobs - limiting DB, network, or CPU concurrency #### Avoid - unbounded goroutine-per-item models when load can spike - worker pools with no stop path - queues that can grow forever with no policy #### Preferred pattern ```go func Run(ctx context.Context, workers int, jobs <-chan Job) error { var wg sync.WaitGroup for i := 0; i < workers; i++ { wg.Add(1) go func() { defer wg.Done() for { select { case <-ctx.Done(): return case job, ok := <-jobs: if !ok { return } job.Process(ctx) } } }() } wg.Wait() return ctx.Err() } ``` #### Anti-pattern - spawning “workers” while also spawning a fresh goroutine per task Explanation: This anti-pattern is common because goroutine-per-task is easy to write, but it defeats the whole resource-control purpose of a worker pool. #### Why - a worker pool is mainly a resource-control pattern --- ### Defer - Path: /topics/defer/index.md - Authority: primary - Last reviewed: 2026-03-26 - Check (must): cleanup is deferred only after resource acquisition succeeds - Check (must): defer is not accidentally accumulating resources inside large loops - Check (should): defer is used to keep cleanup close to setup #### Canonical guidance - call `defer` immediately after successful acquisition of a resource - use it for cleanup paths like `Close`, `Unlock`, and cancellation - remember deferred calls run at function return in LIFO order - avoid `defer` in very hot loops when it is measurable and unnecessary #### Use when - file cleanup - mutex unlocks - context cancellation - tracing and timing hooks #### Avoid - deferring cleanup before checking acquisition errors - piling up defers inside large loops accidentally - replacing normal control flow with defer cleverness #### Preferred pattern ```go f, err := os.Open(name) if err != nil { return err } defer f.Close() ``` #### Anti-pattern ```go for _, name := range names { f, _ := os.Open(name) defer f.Close() } ``` Explanation: This anti-pattern is common because defer is concise, but in loops it can keep files or locks alive far longer than intended. #### Why - defer keeps cleanup close to setup - misuse in loops can retain resources longer than intended --- ### Error comparison - Path: /topics/error-comparison/index.md - Authority: mixed - Last reviewed: 2026-03-26 - Check (must): sentinel-like errors are checked with `errors.Is` when wrapping is possible - Check (must): typed errors are extracted with `errors.As` instead of brittle type assertions on wrapper chains - Check (should): the package has a consistent story for sentinel errors, typed errors, and wrapping #### Canonical guidance - use `errors.Is` for sentinel-style comparisons - use `errors.As` to find a specific error type in a chain - compare directly only when no wrapping or abstraction boundary is involved #### Use when - wrapped errors - retry classification - typed error metadata #### Avoid - `err == target` through wrapper boundaries - type assertions against only the outermost error - mixing many comparison styles without reason #### Preferred pattern ```go if errors.Is(err, context.DeadlineExceeded) { return retryLater() } var pathErr *fs.PathError if errors.As(err, &pathErr) { log.Printf("path failed: %s", pathErr.Path) } ``` #### Anti-pattern - checking only concrete wrapper types and missing the real cause underneath Explanation: This anti-pattern is tempting because direct equality and type assertions are simple, but they break as soon as wrapping enters the picture. #### Why - robust comparison should survive context-rich error propagation --- ### Error handling - Path: /topics/error-handling/index.md - Authority: primary - Last reviewed: 2026-03-26 - Check (must): errors are returned explicitly instead of being swallowed or ignored - Check (should): errors are wrapped with context when propagated upward - Check (must): panic is not used for expected operational failures #### Canonical guidance - return errors explicitly - handle errors close to the point where useful action can be taken - add context when returning upward - keep success path readable by returning early on errors #### Use when - reviewing application control flow - designing library APIs #### Avoid - panic for expected failures - swallowing errors - deeply nested happy-path code caused by delayed error returns #### Preferred pattern ```go u, err := repo.Fetch(ctx, id) if err != nil { return User{}, fmt.Errorf("fetch user %q: %w", id, err) } return u, nil ``` #### Anti-pattern ```go u, _ := repo.Fetch(ctx, id) ``` Explanation: This anti-pattern is tempting in prototypes, but ignored errors destroy observability and push failures into harder-to-debug states. #### Why - explicit error values make failure paths inspectable and composable - contextual wrapping preserves both machine and human usefulness --- ### Error wrapping - Path: /topics/error-wrapping/index.md - Authority: mixed - Last reviewed: 2026-03-26 - Check (should): returned errors add context where it materially helps the caller understand the failure boundary - Check (must): errors meant to remain machine-inspectable are wrapped with `%w` or another compatible wrapper - Check (prefer): errors are not rewrapped at every stack frame without adding meaningful information #### Canonical guidance - add context at boundaries where the operation name matters - use `%w` when callers may need `errors.Is` or `errors.As` - avoid wrapping mechanically when no new information is added #### Use when - I/O boundaries - RPC or database calls - translating low-level failures into higher-level operations #### Avoid - losing the original cause with `%v` - wrapping the same error repeatedly with empty context - exposing internal detail when the boundary should collapse it #### Preferred pattern ```go func loadConfig(path string) error { if err := readConfig(path); err != nil { return fmt.Errorf("read config %q: %w", path, err) } return nil } ``` #### Anti-pattern - returning generic context strings that destroy the original error identity Explanation: This anti-pattern is tempting because any formatted message looks clearer, but callers lose reliable inspection when the original error is no longer wrapped. #### Why - good wrapping keeps both human context and machine-readable cause chains --- ### errors.Join - Path: /topics/errors-join/index.md - Authority: primary - Last reviewed: 2026-03-26 - Check (should): `errors.Join` is used when multiple independent failures from one operation all matter - Check (must): callers can still use `errors.Is` or `errors.As` on the joined result - Check (prefer): joined errors do not replace simpler single-cause errors when one failure is the real boundary #### Canonical guidance - use `errors.Join` when multiple sibling failures deserve to survive - keep joined causes inspectable with `errors.Is` and `errors.As` - prefer a single wrapped error when there is one dominant cause #### Use when - cleanup steps that can each fail - fan-out work reporting multiple relevant failures - validation collecting several independent problems #### Avoid - joining errors mechanically because a slice exists - flattening all context into one opaque string - assuming the display order is an API contract #### Preferred pattern ```go if err1 != nil || err2 != nil { return errors.Join(err1, err2) } return nil ``` #### Anti-pattern - concatenating several error strings into one message and discarding machine-readable causes Explanation: This anti-pattern is tempting because it is quick, but callers lose structured inspection. #### Why - `errors.Join` preserves multiple causes without giving up the normal Go error inspection APIs --- ### Nil interfaces - Path: /topics/nil-interfaces/index.md - Authority: primary - Last reviewed: 2026-03-26 - Check (must): typed nil values inside interfaces are handled deliberately - Check (must): functions do not accidentally return typed nil errors as non-nil error interfaces - Check (should): code and reviews distinguish nil interface from nil concrete value #### Canonical guidance - an interface is `nil` only if it holds no dynamic type and no dynamic value - a typed nil inside an interface is not the same as a nil interface - be careful returning concrete pointer types as `error` #### Use when - debugging surprising `err != nil` - reviewing interface-heavy APIs - explaining nil behavior in Go #### Avoid - assuming a nil pointer assigned to an interface makes the interface nil - returning typed nil errors accidentally #### Preferred pattern ```go if err == nil { return nil } return err ``` #### Anti-pattern ```go var e *MyError = nil return e ``` - if returned as `error`, the interface now has dynamic type `*MyError` Explanation: This anti-pattern is common because a nil pointer looks nil in the debugger, but once wrapped in an interface it can change control flow unexpectedly. #### Why - interface values are represented as type plus value - nil confusion here is a common correctness and API bug --- ### Panic and recover - Path: /topics/panic-and-recover/index.md - Authority: primary - Last reviewed: 2026-03-26 - Check (must): panic is reserved for impossible states or unrecoverable invariants - Check (should): recover is used only at deliberate boundaries that can convert panic into controlled failure - Check (must): panic is not used as normal control flow #### Canonical guidance - panic for impossible states, broken invariants, or initialization failures that must abort - return errors for expected operational failures - recover only at boundaries that can convert a panic into a controlled failure #### Use when - deciding between `panic` and `error` - hardening process or request boundaries #### Avoid - panic for validation failures - recover everywhere - using panic as a substitute for branching #### Preferred pattern ```go func handler(w http.ResponseWriter, r *http.Request) { defer func() { if rec := recover(); rec != nil { http.Error(w, "internal error", http.StatusInternalServerError) } }() serve(w, r) } ``` #### Anti-pattern ```go if err != nil { panic(err) } ``` Explanation: This anti-pattern is tempting because panic is concise, but expected failures deserve explicit control flow and caller-visible error handling. #### Why - panic unwinds control flow aggressively - most failures in real systems are expected enough to deserve explicit error handling --- ### cgo - Path: /topics/cgo/index.md - Authority: primary - Last reviewed: 2026-03-26 - Check (must): cgo is used only for a real interoperability or platform requirement - Check (should): cgo is isolated behind a narrow package boundary - Check (must): cross-language ownership and lifetime rules are explicit #### Canonical guidance - use cgo only for real interoperability or platform needs - isolate cgo behind a narrow package boundary - make ownership, allocation, and lifetime rules explicit - remember cgo affects portability, build complexity, and performance characteristics #### Use when - calling native libraries - binding to platform APIs - bridging to existing C code that cannot be replaced reasonably #### Avoid - casual cgo adoption in otherwise pure-Go projects - leaking C types through wide parts of the codebase - unclear cross-language ownership #### Preferred pattern ```go package sqliteffi /* #cgo pkg-config: sqlite3 #include */ import "C" func Version() string { return C.GoString(C.sqlite3_libversion()) } ``` #### Anti-pattern - letting cgo concerns infect unrelated packages and business logic Explanation: This anti-pattern is tempting during fast integration work, but once C-facing details spread across packages the portability and maintenance costs multiply quickly. #### Why - cgo is a sharp but valid tool - the boundary cost is architectural, not just syntactic --- ### Database timeouts - Path: /topics/database-timeouts/index.md - Authority: primary - Last reviewed: 2026-03-26 - Check (must): database operations in request or job paths use context-aware APIs - Check (should): database deadlines are chosen near the request or job boundary - Check (must): database work does not outlive the caller without an explicit lifecycle #### Canonical guidance - pass context into database operations - derive deadlines from request or job scope, not arbitrary scattered constants - cancel long-running work when the caller no longer needs it - keep timeout decisions near operational boundaries #### Use when - HTTP handlers calling a database - background jobs with deadlines - queue workers #### Avoid - database calls with no cancellation path - timeout logic duplicated inconsistently across layers - detached queries that outlive the request meaningfully #### Preferred pattern ```go ctx, cancel := context.WithTimeout(ctx, 2*time.Second) defer cancel() row := db.QueryRowContext(ctx, q, id) ``` #### Anti-pattern - using `db.QueryRow` or `db.Exec` without context in request-scoped paths Explanation: This anti-pattern is tempting because context-free calls are shorter, but they let slow queries outlive the request or job that needed them. #### Why - database latency is often one of the dominant external costs in Go services - cancellation and deadline propagation reduce tail-latency damage and leaked work --- ### go:embed - Path: /topics/go-embed/index.md - Authority: primary - Last reviewed: 2026-03-26 - Check (must): `//go:embed` patterns are explicit and constrained to the intended files - Check (should): embedded assets are small enough and stable enough that binary growth is acceptable - Check (should): the code chooses between `string`, `[]byte`, and `embed.FS` deliberately #### Canonical guidance - use `//go:embed` for static assets needed at runtime - keep patterns explicit and package-local - choose `embed.FS` when a file tree or `fs.FS` integration matters #### Use when - templates - small static web assets - fixture data that should travel with the binary #### Avoid - embedding huge mutable data sets - broad patterns that pull in unintended files - confusing embed with runtime configuration #### Preferred pattern ```go import "embed" //go:embed templates/*.html var templates embed.FS ``` #### Anti-pattern - embedding development-only assets because it was simpler than fixing the runtime file lookup path Explanation: This anti-pattern is tempting because it removes one deployment problem, but it quietly bloats the binary and hides asset ownership. #### Why - embedding is excellent for stable static assets, not for everything that happens to live on disk --- ### gRPC - Path: /topics/grpc/index.md - Authority: community - Last reviewed: 2026-03-26 - Check (should): gRPC is chosen for contract discipline, streaming, or ecosystem interoperability rather than fashion - Check (must): RPC handlers propagate context and deadlines downstream - Check (should): generated protobuf types are kept at service boundaries when practical #### Canonical guidance - choose gRPC when typed RPC contracts or streaming are central - keep handlers small and push business logic into ordinary Go packages - propagate deadlines and cancellation through service calls - avoid letting generated types leak everywhere unless the boundary is intentionally proto-shaped #### Use when - internal RPC between services - bidirectional or server streaming - strong schema contracts matter #### Avoid - using gRPC by default for simple public HTTP APIs - burying domain logic in generated-service layers - ignoring deadlines on outbound work #### Preferred pattern ```go type UserService struct { pb.UnimplementedUserServiceServer Store Store } func (s *UserService) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.GetUserResponse, error) { user, err := s.Store.Get(ctx, req.Id) if err != nil { return nil, err } return &pb.GetUserResponse{Id: user.ID, Name: user.Name}, nil } ``` #### Anti-pattern - generated handlers that also own persistence, retries, and orchestration logic Explanation: This anti-pattern is tempting because gRPC stubs are already there, but it turns transport code into the whole application boundary. #### Why - gRPC works best when transport contracts stay clean and domain logic stays ordinary --- ### HTTP clients - Path: /topics/http-clients/index.md - Authority: primary - Last reviewed: 2026-03-26 - Check (must): http.Client instances are reused instead of recreated per request - Check (must): request-scoped outbound calls bind requests to context - Check (should): timeouts and transport behavior are configured intentionally #### Canonical guidance - reuse `http.Client` instances - bind requests to context when request-scoped - configure timeouts and transport behavior intentionally - close response bodies promptly #### Use when - service-to-service HTTP - external API integrations - request-scoped outbound calls #### Avoid - creating a new client for every request - no timeout strategy - reading only part of a response and leaking the body #### Preferred pattern ```go req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { return err } resp, err := client.Do(req) if err != nil { return err } defer resp.Body.Close() ``` #### Anti-pattern - default-zero client creation scattered across the codebase with no shared policy Explanation: This anti-pattern is tempting in small helpers, but ad hoc clients and missing body cleanup quietly waste connections and increase latency. #### Why - outbound HTTP is a major source of latency and resource leaks in Go services --- ### HTTP servers - Path: /topics/http-servers/index.md - Authority: primary - Last reviewed: 2026-03-26 - Check (should): server read, write, or idle timeouts are configured intentionally for production services - Check (must): process shutdown has a clear owner that calls `Shutdown` with a bounded context - Check (must): handlers stop request-owned work when the request context is canceled #### Canonical guidance - own the `http.Server` lifecycle explicitly - configure timeouts intentionally - make shutdown and request cancellation part of the design #### Use when - API servers - internal admin servers - background services exposing health or metrics endpoints #### Avoid - `http.ListenAndServe` with no shutdown path in long-lived services - handlers that keep request-owned work running after cancellation - assuming a router framework replaces server lifecycle design #### Preferred pattern ```go srv := &http.Server{ Addr: ":8080", Handler: mux, ReadTimeout: 5 * time.Second, WriteTimeout: 10 * time.Second, } ``` #### Anti-pattern - starting a production server with defaults and no graceful shutdown because "the orchestrator will kill it anyway" Explanation: This anti-pattern is tempting because it reduces boilerplate, but it turns restarts and overload into correctness problems. #### Why - most HTTP server bugs in Go are lifecycle and timeout bugs, not routing bugs --- ### I/O readers and writers - Path: /topics/io-readers/index.md - Authority: primary - Last reviewed: 2026-03-26 - Check (should): streaming APIs use Reader or Writer boundaries when eager byte slices are unnecessary - Check (must): callers and callees are clear about who closes resources and who just reads or writes - Check (prefer): standard io adapters like io.Copy or io.TeeReader are preferred over custom plumbing #### Canonical guidance - accept `io.Reader` or `io.Writer` when data should stream - keep ownership of closing resources explicit - use standard adapters before inventing custom interfaces #### Use when - large payloads - network or file streaming - pipeline-style transformations #### Avoid - forcing everything through `[]byte` - closing readers you did not open unless the contract says so - custom copy loops without a reason #### Preferred pattern ```go func CopyJSON(dst io.Writer, src io.Reader) error { _, err := io.Copy(dst, src) return err } ``` #### Anti-pattern - reading entire streams into memory when incremental processing would do Explanation: This anti-pattern is tempting because byte slices are easy to inspect, but it turns streaming problems into memory problems. #### Why - Reader and Writer interfaces are Go's standard streaming composition points --- ### JSON handling - Path: /topics/json-handling/index.md - Authority: mixed - Last reviewed: 2026-03-26 - Check (should): JSON payloads are decoded into explicit structs by default - Check (must): JSON boundaries validate malformed or surprising input clearly - Check (prefer): the unknown-field policy is explicit rather than accidental #### Canonical guidance - default to explicit struct decoding - validate request boundaries carefully - decide deliberately whether unknown fields should be rejected - keep transport-layer JSON handling separate from domain logic #### Use when - HTTP JSON APIs - config loading - external payload parsing #### Avoid - decoding arbitrary maps unless flexibility is truly needed - silently accepting malformed or surprising payloads at API boundaries - blending decode, validation, and business logic into one large function #### Preferred pattern ```go func decodeCreateUser(r *http.Request) (CreateUserRequest, error) { defer r.Body.Close() var req CreateUserRequest dec := json.NewDecoder(io.LimitReader(r.Body, 1<<20)) dec.DisallowUnknownFields() if err := dec.Decode(&req); err != nil { return CreateUserRequest{}, err } if err := dec.Decode(&struct{}{}); err != io.EOF { return CreateUserRequest{}, errors.New("body must contain a single JSON object") } return req, nil } ``` #### Anti-pattern - generic `map[string]any` decoding across normal application paths Explanation: This anti-pattern is tempting for flexibility, but untyped maps move validation bugs and shape confusion deeper into the application. #### Why - JSON boundaries are where type looseness enters otherwise structured Go code --- ### Logging with slog - Path: /topics/logging-slog/index.md - Authority: primary - Last reviewed: 2026-03-26 - Check (must): log keys are stable and deliberate rather than ad hoc strings - Check (should): request or operation context is attached through structured attributes - Check (should): handler configuration is separated from call-site logging logic #### Canonical guidance - prefer structured keys over free-form concatenated log lines - attach stable request or job context with `With` - keep handler setup in a narrow composition layer #### Use when - service logs - request tracing context - machine-consumable log pipelines #### Avoid - interpolated blob strings with no stable fields - repeating the same attributes at every call site - hiding logger construction deep inside business logic #### Preferred pattern ```go logger := slog.New(slog.NewJSONHandler(os.Stdout, nil)).With( slog.String("service", "api"), ) logger.Info("request complete", slog.Int("status", http.StatusOK)) ``` #### Anti-pattern - unstructured printf logs in code paths that need machine-readable diagnostics Explanation: This anti-pattern is tempting because printf is fast to type, but it throws away the structured context operators depend on. #### Why - structured logs are easier to query, aggregate, and correlate --- ### net/http - Path: /topics/net-http/index.md - Authority: primary - Last reviewed: 2026-03-26 - Check (prefer): net/http is the default starting point unless a concrete gap exists - Check (must): handler work propagates request context downstream - Check (should): server and client timeouts are configured intentionally #### Canonical guidance - start with `net/http` - handlers should be small and explicit about request-scoped work - propagate request context through downstream calls - configure server and client timeouts deliberately #### Use when - building APIs - middleware design - HTTP clients and servers #### Avoid - global shared request state - ignoring request context - default timeouts everywhere in production network code #### Preferred pattern ```go func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { if err := s.handle(r.Context(), w, r); err != nil { http.Error(w, "internal error", http.StatusInternalServerError) } } ``` #### Anti-pattern - handlers that spawn detached goroutines with request-owned resources Explanation: This anti-pattern is tempting when handlers need to keep work running, but detached goroutines often retain request-owned resources after the caller is gone. #### Why - the standard library already encodes the dominant Go HTTP model - many correctness bugs here are lifecycle and timeout bugs, not routing bugs --- ### SQL with database/sql - Path: /topics/sql-database/index.md - Authority: primary - Last reviewed: 2026-03-26 - Check (must): query and exec paths use context-aware database/sql APIs - Check (must): Rows are closed and iteration errors are checked - Check (should): transaction scope matches a real unit of work rather than being too broad #### Canonical guidance - use `QueryContext`, `ExecContext`, and `BeginTx` - close rows promptly and check `rows.Err()` - keep transactions as small as the unit of work allows #### Use when - standard SQL backends - connection pooling via `sql.DB` - request-scoped queries and transactions #### Avoid - context-free database calls in request paths - forgetting to close `Rows` - giant transactions around unrelated work #### Preferred pattern ```go rows, err := db.QueryContext(ctx, q, accountID) if err != nil { return err } defer rows.Close() for rows.Next() { // scan rows } return rows.Err() ``` #### Anti-pattern - opening transactions far above the boundary that knows the actual unit of work Explanation: This anti-pattern is tempting because it centralizes setup, but it holds locks and resources longer than necessary. #### Why - `database/sql` works well when lifetime, context, and transaction scope stay explicit --- ### Tickers - Path: /topics/tickers/index.md - Authority: primary - Last reviewed: 2026-03-26 - Check (must): tickers created for bounded work are stopped when no longer needed - Check (should): one-shot waits use timers or deadlines instead of tickers - Check (should): periodic work has a clear owner and cancellation path #### Canonical guidance - use tickers for repeated work, timers for one-shot waits - stop tickers you create when the owner is done - make periodic work cancellable #### Use when - polling loops - periodic flush or cleanup work - background heartbeats #### Avoid - using tickers for single delayed events - forgetting to stop a ticker in bounded workflows - assuming ticker cadence stays exact under load #### Preferred pattern ```go ticker := time.NewTicker(30 * time.Second) defer ticker.Stop() ``` #### Anti-pattern - starting a ticker in a goroutine with no cancellation path and no clear owner Explanation: This anti-pattern is tempting because periodic work looks harmless, but it quietly creates runaway background loops. #### Why - ticker lifecycle is resource management, not just syntax --- ### Time durations - Path: /topics/time-durations/index.md - Authority: mixed - Last reviewed: 2026-03-26 - Check (must): duration values are expressed with explicit units or parsed duration strings - Check (should): timeout-related call sites make intended units obvious to the reader - Check (should): raw integer-to-duration conversions are avoided unless nanoseconds are truly intended #### Canonical guidance - use constants like `500 * time.Millisecond` - parse human-entered durations with `time.ParseDuration` - be explicit at boundaries where config uses integers #### Use when - timeouts and retries - intervals and backoff - config parsing #### Avoid - `time.Duration(5)` when you mean seconds - ambiguous integer config without documented units - scattering ad hoc conversion logic #### Preferred pattern ```go client := &http.Client{ Timeout: 5 * time.Second, } ``` #### Anti-pattern - reading `5` from config and turning it into `time.Duration(5)` without multiplying by a unit Explanation: This anti-pattern is tempting because `time.Duration` is an integer type, but its default unit is nanoseconds, not whatever the reader hoped. #### Why - time bugs often look fine in code review until they fail under real timing --- ### time.After - Path: /topics/time-after/index.md - Authority: mixed - Last reviewed: 2026-03-26 - Check (should): `time.After` is avoided in repeated loops where explicit timer reuse or cancellation matters - Check (must): reusable or cancelable timeout paths use explicit timers with clear stop or reset behavior - Check (prefer): `time.After` is kept for straightforward one-shot waits where explicit timers would add no value #### Canonical guidance - `time.After` is fine for simple one-off waits - prefer `time.NewTimer` when timeouts repeat or need cleanup control - stop timers you no longer need #### Use when - one-shot `select` timeouts - tests or tiny helpers - code where timer reuse is irrelevant #### Avoid - `time.After` inside long-running loops - creating fresh timers on every iteration without control - assuming timeout channels can be canceled after creation #### Preferred pattern ```go timer := time.NewTimer(timeout) defer timer.Stop() select { case <-done: return nil case <-timer.C: return context.DeadlineExceeded } ``` #### Anti-pattern - `select` loops that create a new `time.After` on every pass Explanation: This anti-pattern is tempting because it is short, but repeated one-shot timer creation obscures lifecycle costs and cancellation behavior. #### Why - explicit timers make repeated timeout paths easier to reason about --- ### unsafe - Path: /topics/unsafe/index.md - Authority: primary - Last reviewed: 2026-03-26 - Check (must): unsafe is used only for a specific justified need - Check (should): unsafe code is isolated behind a narrow boundary - Check (must): layout and lifetime assumptions are documented and testable #### Canonical guidance - prefer safe Go first - use `unsafe` only for a specific, justified need such as low-level interop or measured performance work - keep unsafe regions tiny and heavily constrained - document invariants and assumptions explicitly #### Use when - unavoidable low-level interop - carefully measured hot paths - runtime-adjacent work #### Avoid - using `unsafe` to look clever - relying on undocumented layout assumptions casually - spreading unsafe usage through ordinary business logic #### Preferred pattern ```go type header struct { Len uint32 Kind uint16 } func readHeader(p unsafe.Pointer) header { // p must point to at least unsafe.Sizeof(header{}) bytes of valid memory. return *(*header)(p) } ``` #### Anti-pattern - broad pointer reinterpretation with unclear lifetime and aliasing rules Explanation: This anti-pattern is tempting in hot paths, but once assumptions about layout and lifetime are implicit the code becomes fragile across change. #### Why - unsafe code bypasses the language’s main safety guarantees - the maintenance burden must be justified by concrete benefit --- ### Embedding - Path: /topics/embedding/index.md - Authority: primary - Last reviewed: 2026-03-26 - Check (must): embedding is used for deliberate composition or field promotion - Check (must): embedding is not used as a substitute for inheritance-style design - Check (should): promoted methods and fields are part of the intended API surface #### Canonical guidance - use embedding to compose behavior carefully - understand that promoted methods become part of the outer type's method set - prefer explicit named fields when the relationship is not meant to be promoted #### Use when - composing helper behavior - small wrappers around existing types - deliberate method promotion #### Avoid - treating embedding as inheritance - exporting promoted APIs accidentally - deep chains of embedded types #### Preferred pattern ```go type MetricsServer struct { *http.Server Log *slog.Logger } ``` #### Anti-pattern - embedding many unrelated types into one aggregate “base class” Explanation: This anti-pattern is tempting because it looks concise, but it hides which behavior is truly owned by the outer type. #### Why - embedding changes method sets and API shape, not just field layout --- ### Integer overflow - Path: /topics/integer-overflow/index.md - Authority: mixed - Last reviewed: 2026-03-26 - Check (must): size, length, offset, and counter arithmetic is reviewed for overflow before use - Check (should): code does not assume Go will trap integer overflow automatically - Check (prefer): integer width choices are deliberate where cross-platform or large-range behavior matters #### Canonical guidance - watch arithmetic that feeds allocation sizes, indexing, and protocol lengths - check before the operation when overflow would be unsafe - use fixed-width integers when size guarantees matter #### Use when - counters and accumulators - byte-size calculations - offsets and capacity math #### Avoid - assuming `int` is always large enough - multiplying lengths without bounds checks - converting between widths without considering truncation #### Preferred pattern ```go const maxInt = int(^uint(0) >> 1) func add(a, b int) (int, error) { if b > 0 && a > maxInt-b { return 0, errors.New("overflow") } return a + b, nil } ``` #### Anti-pattern - computing allocation sizes from untrusted input with unchecked arithmetic Explanation: This anti-pattern is tempting because the math looks small and ordinary, but overflow often shows up exactly in boundary calculations. #### Why - overflow bugs corrupt correctness and can become security problems at boundaries --- ### Maps - Path: /topics/maps/index.md - Authority: primary - Last reviewed: 2026-03-26 - Check (should): maps are used when keyed access or membership is the real need - Check (must): code does not write to nil maps - Check (must): maps accessed concurrently are synchronized explicitly #### Canonical guidance - use maps for keyed lookup and set-like membership checks - remember that reading from a nil map is okay but writing is not - synchronize all concurrent access when writes may happen #### Use when - indexing by ID - deduplication - counting and grouping #### Avoid - depending on iteration order - concurrent mutation with no synchronization - concurrent read/write with no synchronization - maps where a slice or struct models the domain more clearly #### Preferred pattern ```go seen := map[string]struct{}{} if _, ok := seen[id]; !ok { seen[id] = struct{}{} } ``` #### Anti-pattern - building behavior that accidentally depends on map iteration order Explanation: This anti-pattern is tempting because test runs can look stable, but order dependencies tend to fail when the data or runtime changes. #### Why - maps are powerful, but their mutation and ordering semantics matter --- ### Named result parameters - Path: /topics/named-result-parameters/index.md - Authority: mixed - Last reviewed: 2026-03-26 - Check (should): named result parameters are used only when they improve clarity or enable a precise defer pattern - Check (must): naked returns are kept rare and only in short obvious functions - Check (should): if a defer mutates a named result, that behavior is obvious from the function body #### Canonical guidance - default to explicit return values - use named results for small functions or precise deferred cleanup - avoid naked returns in longer functions #### Use when - deferred close/rollback needs to update `err` - signatures become clearer with result names - short helper functions #### Avoid - long functions with hidden result mutation - relying on naked returns for brevity - using named results as a style default #### Preferred pattern ```go func run(name string) (err error) { f, err := os.Open(name) if err != nil { return err } defer func() { if cerr := f.Close(); err == nil { err = cerr } }() return useFile(f) } ``` #### Anti-pattern - large functions where deferred logic mutates named results far from the eventual return Explanation: This anti-pattern is tempting because named results can remove repetition, but hidden mutation usually costs more readability than it saves. #### Why - explicit returns keep control flow easier to audit --- ### Nil vs empty slices - Path: /topics/nil-vs-empty-slices/index.md - Authority: mixed - Last reviewed: 2026-03-26 - Check (should): public APIs choose `nil` versus empty slices deliberately rather than incidentally - Check (must): code that serializes slices understands whether `nil` and empty values are encoded differently - Check (prefer): internal code does not over-normalize slices unless an external contract requires it #### Canonical guidance - treat `nil` and empty slices as different only when an API contract or encoder cares - return whichever value best matches the boundary contract - be consistent within one API surface #### Use when - JSON or database boundaries - helper APIs that return collections - compatibility-sensitive responses #### Avoid - converting every `nil` slice to empty by reflex - assuming callers cannot observe the difference - mixing conventions across similar functions #### Preferred pattern ```go func filter(xs []int) []int { var out []int for _, x := range xs { if x%2 == 0 { out = append(out, x) } } return out } ``` #### Anti-pattern - patching slice emptiness repeatedly throughout the codebase instead of deciding at the boundary that needs it Explanation: This anti-pattern is tempting because empty slices feel tidier, but unnecessary normalization adds noise and hides which boundary actually cares. #### Why - most internal code can treat both as empty, but external contracts sometimes cannot --- ### Range loops - Path: /topics/range-loops/index.md - Authority: mixed - Last reviewed: 2026-03-26 - Check (must): code that mutates elements via `range` understands whether it is mutating a copy or the backing collection - Check (should): index-based loops are used when element mutation or address stability matters - Check (should): code does not assume the ranged expression is re-evaluated on every iteration #### Canonical guidance - `range` gives you a copy of the element value - mutate by index when the collection itself must change - remember the ranged expression is evaluated once up front #### Use when - read-only iteration - concise loops over slices, maps, strings, and channels - index plus value traversal #### Avoid - editing struct elements through the copied loop variable - taking addresses of loop variables when element identity matters - assuming appends change what a slice `range` will visit #### Preferred pattern ```go for i := range users { users[i].Active = true } ``` #### Anti-pattern - writing to fields on the ranged value and expecting the original slice element to change Explanation: This anti-pattern is tempting because the loop variable looks like the element, but for most ranges it is just a copy of that value. #### Why - range loops are concise, but their value-copy semantics matter for correctness --- ### Reflection - Path: /topics/reflection/index.md - Authority: primary - Last reviewed: 2026-03-26 - Check (should): reflection is kept near framework or serialization boundaries - Check (must): ordinary static code is preferred when reflection adds no real leverage - Check (must): reflective code checks kind, addressability, and nil cases before acting #### Canonical guidance - prefer static code first - use reflection when the program truly must inspect unknown types at runtime - keep reflective code narrow and well-tested #### Use when - generic serialization or decoding layers - framework hooks - struct-tag-driven behavior #### Avoid - reflection in ordinary request logic - reflective mutation without kind checks - hiding type errors until runtime without strong reason #### Preferred pattern ```go func fieldNames(v any) []string { t := reflect.TypeOf(v) if t.Kind() == reflect.Pointer { t = t.Elem() } var names []string for i := 0; i < t.NumField(); i++ { names = append(names, t.Field(i).Name) } return names } ``` #### Anti-pattern - replacing straightforward concrete code with reflection for no measurable gain Explanation: This anti-pattern is tempting because it feels generic, but it usually trades compile-time clarity for runtime fragility. #### Why - reflection is powerful but expensive in readability and safety margin --- ### Slices - Path: /topics/slices/index.md - Authority: primary - Last reviewed: 2026-03-26 - Check (must): slice aliasing and shared backing arrays are understood at API boundaries - Check (should): code relies on append semantics deliberately, not accidentally - Check (should): slices are copied before long-term ownership when callers should not share backing storage #### Canonical guidance - remember slices share backing arrays until a copy or reallocation breaks sharing - use `copy` when ownership should be distinct - reason about length and capacity when appending #### Use when - ordered collections - buffers - batched processing #### Avoid - returning subslices that accidentally retain large backing arrays - mutating shared slices without clear ownership - assuming append always mutates in place #### Preferred pattern ```go func Clone(in []byte) []byte { out := make([]byte, len(in)) copy(out, in) return out } ``` #### Anti-pattern - keeping a tiny subslice of a huge buffer alive for a long time Explanation: This anti-pattern is tempting because subslices are cheap, but they can retain far more memory than intended. #### Why - slice semantics are simple once ownership and aliasing stay explicit --- ### Strings - Path: /topics/strings/index.md - Authority: primary - Last reviewed: 2026-03-26 - Check (must): strings are treated as immutable values - Check (should): code chooses byte-oriented or rune-oriented behavior deliberately - Check (should): UTF-8 assumptions are explicit when indexing or slicing strings #### Canonical guidance - strings are immutable - bytes and runes solve different problems - slicing a string operates on bytes, not characters #### Use when - text processing - protocol fields - log and error messages #### Avoid - indexing strings as if every character were one byte - converting between string and `[]byte` repeatedly without reason - assuming all input is valid UTF-8 #### Preferred pattern ```go for _, r := range s { fmt.Println(r) } ``` #### Anti-pattern - slicing strings by byte offsets when the logic really cares about user-visible characters Explanation: This anti-pattern is tempting because byte indexing is simple, but it breaks once multibyte UTF-8 input appears. #### Why - string handling bugs are often really byte-vs-rune bugs --- ### Struct tags - Path: /topics/struct-tags/index.md - Authority: primary - Last reviewed: 2026-03-26 - Check (should): struct tags are used for serialization, validation, or framework boundaries rather than core domain behavior - Check (must): tag keys and meanings stay stable and documented - Check (prefer): tag-driven behavior is limited when explicit code would be clearer #### Canonical guidance - use tags for narrow metadata at boundaries - keep tag semantics stable and well-known - prefer explicit code when tags start encoding too much behavior #### Use when - JSON or DB mapping - validation libraries - framework or serialization hints #### Avoid - tags that act like a second programming language - hidden business rules encoded only in metadata - many overlapping tag systems on one type #### Preferred pattern ```go type User struct { ID string `json:"id"` Name string `json:"name"` } ``` #### Anti-pattern - relying on undocumented tag strings to drive large parts of application behavior Explanation: This anti-pattern is tempting because tags look lightweight, but hidden metadata quickly becomes hard to reason about and refactor. #### Why - struct tags work best as a small boundary hint, not a logic engine --- ### Type assertions - Path: /topics/type-assertions/index.md - Authority: primary - Last reviewed: 2026-03-26 - Check (must): optional assertions use the comma-ok form or a type switch - Check (should): single-result assertions that can panic are reserved for cases proven by surrounding invariants - Check (should): type assertions appear only where runtime type variation is genuinely expected #### Canonical guidance - use the comma-ok form when failure is possible - prefer type switches for multiple expected concrete types - avoid interface plumbing that forces unnecessary assertions later #### Use when - boundary code with multiple concrete implementations - decoding into interface-shaped values - bridging plugin or handler registries #### Avoid - panic-prone assertions in ordinary code paths - asserting types because APIs are too vague - asserting through several layers of interfaces #### Preferred pattern ```go if u, ok := v.(User); ok { return u.ID } return "" ``` #### Anti-pattern - relying on `v.(T)` in normal control flow when a failed assertion is a valid possibility Explanation: This anti-pattern is tempting because the syntax is short, but it turns a common branch into a runtime panic. #### Why - assertions are safe when the failure path is part of the design, not an afterthought --- ### Build tags - Path: /topics/build-tags/index.md - Authority: primary - Last reviewed: 2026-03-26 - Check (must): build constraints represent real platform, toolchain, or feature differences - Check (must): go:build syntax is used consistently in modern code - Check (should): build tags are not used as a substitute for runtime configuration #### Canonical guidance - use build constraints for real compile-time differences - prefer `//go:build` in modern code - keep constrained files small and obvious - choose runtime configuration when behavior should vary without recompilation #### Use when - OS- or architecture-specific files - optional cgo integrations - toolchain-specific implementations #### Avoid - hiding ordinary business logic behind build tags - complex boolean expressions without clear need - using build tags for per-environment configuration #### Preferred pattern ```go //go:build linux package poller ``` #### Anti-pattern - scattering application features across many build-tag combinations Explanation: This anti-pattern is tempting when feature flags feel compile-time friendly, but it makes builds harder to reason about and test. #### Why - build constraints are for compile-time portability and toolchain boundaries --- ### go and toolchain directives - Path: /topics/go-and-toolchain-directives/index.md - Authority: primary - Last reviewed: 2026-03-26 - Check (must): the `go` directive reflects the minimum language or standard-library expectation the module truly requires - Check (should): the `toolchain` directive is used for developer convenience, not to hide an inaccurate `go` directive - Check (should): `go` and `toolchain` are reviewed deliberately during version upgrades #### Canonical guidance - keep `go` honest about the minimum version the module needs - use `toolchain` only when the default developer toolchain should be newer - review both directives when adopting new language or library features #### Use when - upgrading Go versions - enabling new language features - standardizing local toolchains across contributors #### Avoid - bumping `go` casually without using the new requirement - using `toolchain` to paper over incorrect version requirements - forgetting that older consumers read the `go` directive as compatibility signal #### Preferred pattern ```toml module example.com/app go 1.23 toolchain go1.25.3 ``` #### Anti-pattern - setting `go` to the newest company toolchain even though the module only needs an older language baseline Explanation: This anti-pattern is tempting because standardization feels neat, but it raises the compatibility floor without a real reason. #### Why - these directives communicate compatibility and toolchain expectations to both humans and tooling --- ### Go workspaces - Path: /topics/go-workspaces/index.md - Authority: primary - Last reviewed: 2026-03-26 - Check (must): `go.work` is used for local multi-module development, not as a substitute for published module requirements - Check (must): each module remains buildable and testable without requiring downstream consumers to use the workspace - Check (should): workspace membership stays intentional instead of accreting unrelated modules #### Canonical guidance - use `go.work` when developing several modules together - keep each module's `go.mod` authoritative for consumers - treat the workspace file as local coordination, not public version policy #### Use when - monorepos with several Go modules - coordinated local refactors across modules - testing unpublished module combinations #### Avoid - assuming CI or consumers must use the same workspace - using `go.work` to hide broken module requirements - letting unrelated experimental modules leak into the workspace #### Preferred pattern ```bash go work init ./lib ./service go work use ./tools ``` #### Anti-pattern - depending on `go.work` to make a module build because its `go.mod` no longer states the real requirements Explanation: This anti-pattern is tempting during local iteration, but it breaks reproducibility for everyone outside the workspace. #### Why - workspaces are for local multi-module editing, not for published dependency contracts --- ### govulncheck - Path: /topics/govulncheck/index.md - Authority: primary - Last reviewed: 2026-03-26 - Check (must): `govulncheck` runs regularly in development or CI for module-aware codebases - Check (must): findings are triaged based on actual reachability and impact rather than package presence alone - Check (should): accepted risks or ignored findings are documented with rationale and review date #### Canonical guidance - run `govulncheck` regularly - prioritize reachable vulnerabilities over raw dependency presence - document remediation or accepted-risk decisions #### Use when - CI security checks - dependency upgrades - incident or audit preparation #### Avoid - treating every advisory as equally actionable - ignoring findings because the dependency is indirect - replacing ordinary upgrade policy with scanner output alone #### Preferred pattern ```bash govulncheck ./... ``` #### Anti-pattern - suppressing all findings because the first report contained one unreachable advisory Explanation: This anti-pattern is tempting because false urgency is frustrating, but reachability-aware triage is the point of the tool. #### Why - `govulncheck` adds security signal that is specific to Go call paths and module metadata --- ### Minimal version selection - Path: /topics/minimal-version-selection/index.md - Authority: primary - Last reviewed: 2026-03-26 - Check (must): dependency upgrades happen explicitly instead of assuming the newest transitive version will be selected automatically - Check (should): `require` lines are reviewed as input to version selection rather than ignored as boilerplate - Check (prefer): reviews do not assume the build will drift to the latest compatible release on its own #### Canonical guidance - Go selects the minimum version needed to satisfy module requirements - upgrade dependencies intentionally - read `go.mod` changes as version-selection policy, not just lockfile noise #### Use when - debugging dependency versions - reviewing module diffs - planning upgrades across large repos #### Avoid - assuming transitive dependencies float to the latest release - treating `go mod tidy` output as unimportant - expecting semver ranges like other ecosystems #### Preferred pattern ```bash go get example.com/lib@v1.4.2 go mod tidy ``` #### Anti-pattern - assuming a security or bugfix release is active because it exists upstream, even though no requirement in the graph demands it Explanation: This anti-pattern is tempting because many ecosystems float versions, but Go's module solver is intentionally more predictable. #### Why - minimal version selection trades surprise-free builds for explicit upgrade responsibility --- ### Module compatibility - Path: /topics/module-compatibility/index.md - Authority: primary - Last reviewed: 2026-03-26 - Check (must): breaking exported API changes are identified deliberately - Check (must): real incompatibilities trigger proper major-version handling - Check (should): compatible evolution is preferred over unnecessary breakage #### Canonical guidance - preserve compatibility when you can - breaking API changes deserve deliberate version boundaries - import compatibility and semantic import versioning are core constraints, not paperwork #### Use when - evolving public libraries - planning major-version changes - reviewing whether a change is actually breaking #### Avoid - silent breaking changes in minor releases - using modules without understanding import compatibility - treating versioning as only a package manager concern #### Preferred pattern ```go type Client struct{} func (c *Client) Do(ctx context.Context, req *Request) (*Response, error) { return nil, nil } func (c *Client) DoWithRetry(ctx context.Context, req *Request, retries int) (*Response, error) { return nil, nil } ``` #### Anti-pattern - changing exported contracts in place and expecting downstream users to absorb it Explanation: This anti-pattern is tempting during rapid iteration, but silent breakage turns every downstream upgrade into manual triage. #### Why - compatibility is a large part of Go’s developer experience promise --- ### Modules - Path: /topics/modules/index.md - Authority: primary - Last reviewed: 2026-03-26 - Check (must): go.mod and go.sum are committed and treated as the module contract - Check (must): major version import path rules are followed for v2 and beyond - Check (should): dependency changes remain explicit and reviewable #### Canonical guidance - use `go.mod` as the declared module boundary - commit `go.mod` and `go.sum` - keep dependencies explicit and reviewable - understand major version suffix rules for `v2+` - route workspace, replacement, vendoring, and private-module policy questions to dedicated narrower pages #### Use when - starting any modern Go repo - publishing libraries - upgrading dependencies #### Avoid - ad hoc dependency management outside modules - ignoring semantic import versioning for `v2+` - treating transitive dependency state as invisible #### Preferred pattern ```go package main import "example.com/acme/widget/v2" func main() { _ = widget.NewClient() } ``` #### Anti-pattern - shipping a `v2` module at the old import path Explanation: This anti-pattern is tempting when release pressure is high, but ignoring import versioning breaks consumers in ways tooling cannot smooth over. #### Why - modules are the compatibility and reproducibility contract for Go dependencies --- ### Private modules and proxies - Path: /topics/private-modules-and-proxies/index.md - Authority: primary - Last reviewed: 2026-03-26 - Check (must): `GOPRIVATE` covers the organization's private module path prefixes - Check (should): proxy or checksum bypass is scoped only to private paths rather than disabled globally - Check (prefer): public dependencies continue to use the module proxy and checksum database unless policy requires otherwise #### Canonical guidance - set `GOPRIVATE` for private module path prefixes - scope `GONOSUMDB` or `GONOPROXY` narrowly when needed - keep proxy and checksum protections for public modules #### Use when - internal company modules - mixed public and private dependency graphs - corporate proxy or checksum policy configuration #### Avoid - disabling checksum verification for everything - checking secrets into `go env -w` scripts - debugging private module access by globally turning off protections #### Preferred pattern ```bash go env -w GOPRIVATE=example.com,github.com/acme/* ``` #### Anti-pattern - setting `GONOSUMDB=*` or `GONOPROXY=*` just to make one private dependency work Explanation: This anti-pattern is tempting under delivery pressure, but it throws away safety for the entire dependency graph. #### Why - private module access is a policy boundary; it should be explicit and narrowly scoped --- ### Replace directives - Path: /topics/replace-directives/index.md - Authority: primary - Last reviewed: 2026-03-26 - Check (must): every `replace` has a clear reason such as local development, a reviewed fork, or a temporary workaround - Check (must): local filesystem replacements are not committed when they would break reproducible builds for others - Check (should): committed fork replacements document why the fork exists and when it should be removed #### Canonical guidance - use `replace` deliberately, not casually - prefer short-lived local replacements for local iteration - if a committed replacement points to a fork, document the reason and exit path #### Use when - patching a dependency locally - testing an unpublished sibling module without a workspace - pinning to a reviewed fork #### Avoid - committing personal filesystem paths - hiding dependency drift behind long-lived unexplained replacements - using `replace` as normal version-management policy #### Preferred pattern ```toml module example.com/app replace example.com/lib => ../lib ``` #### Anti-pattern - leaving a local `../fork` replacement in committed `go.mod` for a shared repository Explanation: This anti-pattern is tempting because it works on one machine, but it breaks reproducibility everywhere else. #### Why - `replace` is powerful, but it changes dependency resolution in ways other developers and CI must be able to explain --- ### Vendoring - Path: /topics/vendoring/index.md - Authority: primary - Last reviewed: 2026-03-26 - Check (must): `vendor/` is generated from module metadata and not hand-edited - Check (should): the repository has a clear reason for vendoring such as hermetic builds, audit policy, or restricted networks - Check (must): changes to dependencies refresh `vendor/` together with module metadata #### Canonical guidance - vendor only when it solves a real build or policy problem - treat `vendor/` as generated state - refresh vendored code whenever dependency intent changes #### Use when - hermetic or offline builds - strict dependency review environments - build systems that require vendored inputs #### Avoid - editing vendored code in place - assuming vendoring replaces dependency review - committing stale `vendor/` trees after module changes #### Preferred pattern ```bash go mod tidy go mod vendor ``` #### Anti-pattern - patching a vendored dependency by hand and forgetting the real source of truth is elsewhere Explanation: This anti-pattern is tempting because it is fast, but the patch disappears or diverges the next time vendoring is regenerated. #### Why - vendoring is a build artifact with policy implications, not an alternate package-management model --- ### Benchmarks - Path: /topics/benchmarks/index.md - Authority: primary - Last reviewed: 2026-03-26 - Check (should): benchmarks measure representative operations rather than synthetic empty work - Check (should): benchmark setup is separated from measured work - Check (prefer): profiles are used to investigate meaningful benchmark costs before redesigning code #### Canonical guidance - benchmark representative operations - compare real alternatives, not empty shells - keep setup and measured work separated carefully - use profiling when the benchmark shows meaningful cost #### Use when - performance comparisons - regression tracking - validating optimization claims #### Avoid - unrealistic microbenchmarks divorced from production behavior - reading benchmark output without understanding allocations and setup cost - changing code for benchmark wins that harm clarity without real impact #### Preferred pattern ```go func BenchmarkParse(b *testing.B) { for i := 0; i < b.N; i++ { _ = Parse(input) } } ``` #### Anti-pattern - benchmark code that accidentally measures initialization more than the target operation Explanation: This anti-pattern is common because benchmark harness code is easy to mix with setup, but that inflates numbers and makes comparisons meaningless. #### Why - benchmark numbers are only useful when the workload shape is credible --- ### Escape analysis - Path: /topics/escape-analysis/index.md - Authority: primary - Last reviewed: 2026-03-26 - Check (should): escape analysis output is used to investigate measured allocation issues - Check (prefer): public APIs are not distorted purely to satisfy current compiler escape heuristics - Check (must): allocation changes are validated with measurement rather than compiler output alone #### Canonical guidance - use escape analysis as a diagnostic, not as the whole optimization strategy - measure allocations before and after changes - avoid twisting APIs around fragile compiler details without clear payoff #### Use when - heap-allocation investigation - surprising allocation hot spots - validating low-level optimization ideas #### Avoid - assuming stack allocation is always worth API complexity - reading `-gcflags=-m` output with no benchmark or profile - micro-optimizing untouched code paths #### Preferred pattern ```bash go test -run '^$' -bench . -benchmem go build -gcflags='-m=2' ./... ``` #### Anti-pattern - rewriting a clear API into pointer gymnastics because one compiler report mentioned an escape Explanation: This anti-pattern is tempting because compiler diagnostics feel precise, but performance work still needs end-to-end measurement. #### Why - escape analysis is a useful clue, not a substitute for profiling and benchmarking --- ### Execution tracing - Path: /topics/execution-tracing/index.md - Authority: primary - Last reviewed: 2026-03-26 - Check (should): execution traces are used when the problem involves interactions between goroutines, scheduler behavior, blocking, or GC - Check (should): trace capture windows are narrow enough to isolate the behavior under investigation - Check (prefer): trace findings are correlated with profiles, benchmarks, or logs rather than read in isolation #### Canonical guidance - use traces for interaction-heavy latency problems - capture focused traces around the suspect window - combine trace evidence with profiles and benchmarks #### Use when - scheduler delays - lock contention or blocking - GC pauses affecting latency - bursty request-path investigations #### Avoid - capturing giant traces first and hoping the answer appears - using tracing when a simple CPU or heap profile would suffice - interpreting a trace without a concrete question #### Preferred pattern ```bash go test -trace trace.out ./... go tool trace trace.out ``` #### Anti-pattern - treating execution tracing as the default first step for every performance question Explanation: This anti-pattern is tempting because traces are rich, but they are heavier and harder to read than simpler tools. #### Why - tracing shines when the bug is about interactions over time, not just aggregate resource cost --- ### Garbage collector - Path: /topics/garbage-collector/index.md - Authority: primary - Last reviewed: 2026-03-26 - Check (must): GC tuning is driven by measurement rather than folklore - Check (should): allocation behavior is examined before changing GC knobs - Check (must): GOGC and related settings are not changed without workload evidence #### Canonical guidance - reduce unnecessary allocations before tuning GC knobs - understand the relationship between allocation rate, live heap, and CPU overhead - use the official GC guide as the baseline mental model - tune only after measurement demonstrates a need #### Use when - memory pressure - GC CPU spikes - latency-sensitive services #### Avoid - treating GC tuning as a first-line fix - cargo-culting `GOGC` values - assuming every allocation issue is a collector issue #### Preferred pattern ```go func BenchmarkDecode(b *testing.B) { payload := []byte(`{"name":"gopher"}`) b.ReportAllocs() for i := 0; i < b.N; i++ { var req Request if err := json.Unmarshal(payload, &req); err != nil { b.Fatal(err) } } } ``` #### Anti-pattern - lowering `GOGC` or raising it dramatically without workload data Explanation: This anti-pattern is common because GC knobs look like easy wins, but without workload data they usually mask the real allocation problem. #### Why - most GC wins come from allocation behavior and workload understanding, not magic settings --- ### Profiling - Path: /topics/profiling/index.md - Authority: primary - Last reviewed: 2026-03-26 - Check (must): performance changes are driven by measurement - Check (should): pprof or tracing is chosen based on the bottleneck being investigated - Check (must): changes are verified by re-measuring after optimization #### Canonical guidance - optimize only after measurement - use CPU, heap, alloc, block, mutex, and trace data as needed - prefer trace when latency depends on scheduler or blocking interactions - identify the real bottleneck before redesigning code - use PGO after stable measurement shows it helps #### Use when - latency regressions - CPU spikes - memory growth - suspected allocation overhead #### Avoid - micro-optimizing without profiles - trusting intuition over measurements - only benchmarking synthetic happy paths #### Preferred pattern ```go package debugserver import ( "log" "net/http" _ "net/http/pprof" ) func Start() { go func() { log.Println(http.ListenAndServe("localhost:6060", nil)) }() } ``` #### Anti-pattern - rewriting APIs around assumed performance problems with no data Explanation: This anti-pattern is tempting because intuition is faster than instrumentation, but it often optimizes the wrong thing and adds complexity. #### Why - Go gives strong built-in tooling; skipping it usually wastes time --- ### Coverage - Path: /topics/coverage/index.md - Authority: primary - Last reviewed: 2026-03-26 - Check (should): coverage is collected for the packages and execution paths that matter, not just the easiest unit-test surface - Check (must): coverage numbers are not treated as quality proof or gamed with shallow assertions - Check (prefer): integration or multi-package coverage is measured when the critical behavior crosses package boundaries #### Canonical guidance - use coverage to discover untested behavior, not to declare success - measure the packages and paths that matter - prefer readable gaps over vanity percentages #### Use when - reviewing test blind spots - comparing unit and integration coverage - validating critical package boundaries #### Avoid - chasing 100 percent for its own sake - assuming line coverage proves behavior coverage - excluding hard cases just to keep numbers high #### Preferred pattern ```bash go test -cover ./... go test -coverpkg=./... -coverprofile=cover.out ./... ``` #### Anti-pattern - merging a coverage threshold rule that the team satisfies with shallow tests nobody trusts Explanation: This anti-pattern is tempting because one number is easy to govern, but it is a weak proxy for real confidence. #### Why - coverage is a diagnostic input, not a substitute for good test design --- ### Fuzzing - Path: /topics/fuzzing/index.md - Authority: mixed - Last reviewed: 2026-03-26 - Check (should): fuzzing is aimed at parsers, codecs, and transforms where many surprising inputs exist - Check (must): fuzz targets assert stable properties or safety expectations, not only a few example outputs - Check (prefer): seed inputs include known tricky or regression-triggering cases #### Canonical guidance - fuzz boundary-heavy code, not everything - assert invariants and safety properties - keep seed corpora small but interesting #### Use when - parsing and decoding - normalization and round-tripping - string or byte transforms #### Avoid - fuzzing code with trivial input space - writing fuzz targets with no meaningful property to check - replacing ordinary unit tests with fuzzing #### Preferred pattern ```go func FuzzParseDuration(f *testing.F) { f.Add("5s") f.Fuzz(func(t *testing.T, s string) { _, _ = time.ParseDuration(s) }) } ``` #### Anti-pattern - assuming table-driven tests cover the strange inputs users, files, or networks will eventually produce Explanation: This anti-pattern is tempting because hand-picked examples feel complete, but fuzzing is good at finding the cases humans fail to imagine. #### Why - fuzzing complements deterministic tests by exploring the unexpected --- ### Golden files and testdata - Path: /topics/golden-files-and-testdata/index.md - Authority: primary - Last reviewed: 2026-03-26 - Check (must): test fixtures and golden files live under `testdata/` or another deliberate test-only location - Check (should): golden file updates happen through an explicit opt-in workflow rather than silently during normal tests - Check (prefer): fixture formats stay human-reviewable when practical so diffs remain meaningful #### Canonical guidance - keep external fixtures under `testdata/` - make golden updates explicit - choose fixture formats that are easy to diff and review when practical #### Use when - parser outputs - rendered templates - protocol fixtures - stable serialized responses #### Avoid - scattering fixtures through package roots - rewriting golden files during ordinary test runs - huge opaque binary goldens when a smaller text fixture would do #### Preferred pattern ```go want, err := os.ReadFile("testdata/want.json") if err != nil { t.Fatal(err) } ``` #### Anti-pattern - auto-updating every golden file whenever a test fails because it is faster than deciding whether the behavior change is correct Explanation: This anti-pattern is tempting because it keeps tests green, but it destroys the review value of fixtures. #### Why - fixtures are useful only when changes are intentional and inspectable --- ### Subtests - Path: /topics/subtests/index.md - Authority: primary - Last reviewed: 2026-03-26 - Check (should): subtests use meaningful names for failure output - Check (prefer): subtests organize related cases without unnecessary nesting - Check (must): subtest structure keeps failure output clear #### Canonical guidance - use `t.Run` to name and isolate related cases - combine subtests with table-driven structure when it improves clarity - keep subtest names meaningful for failure output #### Use when - grouped scenarios - shared test harness with many variants - selective test execution #### Avoid - deeply nested subtests that obscure control flow - using subtests when a simple loop or a separate test would be clearer #### Preferred pattern ```go t.Run("invalid input", func(t *testing.T) { // ... }) ``` #### Anti-pattern - anonymous or content-free subtest names that make failure output useless Explanation: This anti-pattern is common because subtests are easy to nest, but excessive nesting obscures which behavior actually failed. #### Why - subtests improve diagnostics and organization when used sparingly and clearly --- ### Table-driven tests - Path: /topics/table-driven-tests/index.md - Authority: primary - Last reviewed: 2026-03-26 - Check (should): table-driven tests are used when cases share the same structure - Check (should): cases are named when that improves failure diagnosis - Check (prefer): table-driven style is not forced onto unrelated scenarios #### Canonical guidance - use a table when cases differ mainly by input and expected output - name cases when failure diagnosis matters - combine with subtests when case isolation improves output #### Use when - parser tests - validation matrices - boundary and edge-case suites #### Avoid - forcing table-driven style onto tests with very different setup or behavior - giant anonymous case tables with unclear intent #### Preferred pattern ```go tests := []struct { name string in string want int }{ {name: "empty", in: "", want: 0}, } ``` #### Anti-pattern - one enormous table spanning unrelated behavior domains Explanation: This anti-pattern is common because the pattern is popular, but forcing unrelated scenarios into one table hides intent and makes cases harder to maintain. #### Why - table-driven tests reduce duplication while keeping case coverage explicit --- ### Test execution modes - Path: /topics/test-execution-modes/index.md - Authority: mixed - Last reviewed: 2026-03-26 - Check (should): tests use parallel execution deliberately where isolation is real and shared state is controlled - Check (should): the test workflow uses shuffle or other ordering perturbation to catch hidden coupling - Check (prefer): flags such as `-run`, `-short`, and parallel controls are used to shape local and CI feedback loops #### Canonical guidance - use `t.Parallel` only for tests that are truly isolated - run with shuffle sometimes to catch order dependence - use `-short` and `-run` to control feedback loops deliberately #### Use when - large suites - flaky ordering suspicions - separating slow integration tests from fast unit tests #### Avoid - marking stateful tests parallel by default - assuming deterministic test order matters - making one CI mode do every job #### Preferred pattern ```go func TestParse(t *testing.T) { t.Parallel() got, err := Parse("x=1") if err != nil { t.Fatal(err) } if got["x"] != "1" { t.Fatalf("x = %q", got["x"]) } } ``` #### Anti-pattern - a suite that only passes because tests happen to run in a lucky order Explanation: This anti-pattern is tempting because stable order hides shared-state bugs, but real test confidence comes from independence, not luck. #### Why - execution modes are cheap ways to surface hidden coupling --- ### Test helpers - Path: /topics/test-helpers/index.md - Authority: primary - Last reviewed: 2026-03-26 - Check (must): helper functions that fail tests call `t.Helper()` - Check (should): helper extraction removes repetition without hiding the important behavior under test - Check (prefer): helpers stay small instead of growing into an opaque internal assertion framework #### Canonical guidance - mark failing helpers with `t.Helper()` - keep helpers narrow and behavior-focused - let the test body still read like the scenario being validated #### Use when - fixture setup - repeated assertions with good failure messages - shared test server or temporary directory helpers #### Avoid - helpers that hide all the meaningful assertions - custom assertion DSLs that are harder to debug than plain Go - passing `testing.T` everywhere when plain values would suffice #### Preferred pattern ```go func mustTempFile(t *testing.T, body string) string { t.Helper() f, err := os.CreateTemp(t.TempDir(), "*.txt") if err != nil { t.Fatal(err) } if _, err := f.WriteString(body); err != nil { t.Fatal(err) } return f.Name() } ``` #### Anti-pattern - building a huge custom helper layer that makes ordinary test failures impossible to trace Explanation: This anti-pattern is tempting because reuse feels clean, but tests lose readability and local debugging signal. #### Why - small helpers reduce noise while preserving the directness of ordinary Go tests --- ### Testing - Path: /topics/testing/index.md - Authority: primary - Last reviewed: 2026-03-26 - Check (must): tests are deterministic and do not depend on hidden timing or ordering - Check (should): failure messages clearly identify the broken case and expectation - Check (should): tests focus on behaviorally meaningful boundaries rather than private trivia #### Canonical guidance - prefer small, deterministic tests - use table-driven tests when multiple cases share the same structure - make failure messages identify the case and the violated expectation - test behavior at meaningful boundaries, not private implementation trivia - route helper, fixture, coverage, and concurrency-timing concerns to narrower testing pages #### Use when - unit tests - subtests - regression tests #### Avoid - brittle tests tied tightly to internals - giant test functions with many unrelated scenarios - hidden dependencies on wall-clock time or execution order #### Preferred pattern ```go for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { got := Parse(tc.in) if got != tc.want { t.Fatalf("Parse(%q) = %q, want %q", tc.in, got, tc.want) } }) } ``` #### Anti-pattern - opaque assertions that fail without telling you which case broke Explanation: This anti-pattern is tempting because terse assertions look clean, but weak diagnostics slow down debugging and make regressions harder to understand. #### Why - Go testing works best when cases are explicit, reproducible, and easy to extend --- ### testing/synctest - Path: /topics/testing-synctest/index.md - Authority: primary - Last reviewed: 2026-03-26 - Check (should): `testing/synctest` is used for timing-sensitive concurrent behavior that would otherwise rely on real sleeps or scheduler luck - Check (must): tests inside a synctest bubble do not casually mix fake-time assumptions with unrelated real-time waiting - Check (should): the test makes the synctest bubble boundary and expected synchronization points obvious #### Canonical guidance - use `testing/synctest` for deterministic concurrent tests - prefer fake time and controlled scheduling over flaky sleeps - keep the bubble scope obvious #### Use when - retry loops with timers - background goroutines that need deterministic advancement - concurrency code that flakes under scheduler timing #### Avoid - sleeping real milliseconds in tests that only need synchronization - wrapping every concurrency test in synctest when plain channels are enough - unclear bubble boundaries #### Preferred pattern ```go synctest.Run(func() { // run concurrent code inside a deterministic bubble }) ``` #### Anti-pattern - adding `time.Sleep(50 * time.Millisecond)` until a flaky concurrent test "usually" passes Explanation: This anti-pattern is tempting because it is easy, but it replaces correctness with timing luck. #### Why - deterministic concurrency tests are faster and more trustworthy than scheduler-dependent sleeps ---