Go Patterns & Idioms Best Practices
A condensed summary of the 25 most important Go patterns and idioms practices drawn from every page in this section.
Search across all documentation pages
A condensed summary of the 25 most important Go patterns and idioms practices drawn from every page in this section.
Composition first: Model behavior with structs, methods, embedding, and functions instead of subclass hierarchies copied from Java or C#.
Consumer-defined interfaces: Declare interfaces where behavior is used (service package), not beside every concrete implementation.
Keep interfaces small: One or two methods per boundary (Store, Clock, Publisher) make fakes trivial and APIs stable.
Accept interfaces, return structs: Constructor parameters depend on minimal behavior; New returns *Concrete so callers are not locked into your interface shape.
Unexported state: Library types hide fields behind methods and options so invariants cannot be broken after construction.
Name constructors idiomatically: Use New for fresh values, Open/Dial for resources, and pair with Close/Shutdown when cleanup is required.
Document zero value policy: State clearly whether var T is usable (bytes.Buffer) or requires New() (Store with private maps).
Functional options for growth: Apply the options pattern when constructors would otherwise sprawl across many orthogonal parameters.
Validate in New: Return errors from constructors and options when configuration can fail; avoid deferred panics on first use.
Defaults inside New: Set sensible defaults before applying options so callers override only what they need.
Thin HTTP handlers: Parse transport, call one service method, map errors to status codes - no SQL or branching business rules.
Services own rules: Cancellation policy, authorization, idempotency, and multi-step workflows live in service methods.
Repositories own I/O: SQL, SDK calls, and row mapping stay behind OrderRepo-style interfaces in internal storage packages.
Context everywhere on I/O: Pass context.Context as the first parameter on handlers, services, and repo methods that may block.
Middleware for cross-cutting only: Logging, request IDs, recovery, and auth credential parsing belong in middleware - not pricing logic.
Typed context keys: Use private type ctxKey int constants instead of string keys to avoid collisions between middleware packages.
Call next in middleware: Invoke next.ServeHTTP unless the middleware fully wrote the response; document middleware order in main.
sync.Once for one-time setup: Guard expensive lazy initialization (compiled regex, default registry) when dependency injection is impractical.
Avoid init() I/O: Do not dial databases, read secrets, or parse config in init() - wire resources in main or explicit constructors.
Limit package globals: Prefer injecting *sql.DB, loggers, and metrics registries from main instead of Default() singletons.
Embed deliberately: Embed to promote methods on thin decorators (http.Handler), not to simulate inheritance trees.
Errors not panic: Return error for validation and expected failures; reserve panic for programmer bugs and startup Must* helpers.
Map errors at the edge: Services return domain errors; HTTP/gRPC adapters translate with errors.Is and errors.As.
Test with fakes: Implement consumer interfaces in _test.go to exercise services without Docker or cloud credentials.
Least clever pattern wins: Reach for options, repositories, or middleware when a second axis of change appears - not before.
Keep handlers thin and push rules into services behind small interfaces - most other practices support that boundary.
When more than one exported constructor in your module needs optional settings that would otherwise break callers on every new field.
Enforce import rules in review: domain packages must not import SQL drivers or HTTP routers; wire in cmd/ and internal/.
Rarely in application code; prefer injection.
Library-level immutable lazy caches with sync.Once are the main exception.
Middleware and handler composition here apply directly to stdlib servers and chi; gin/echo follow the same ideas with different types.
Stack versions: This page was written for Go 1.26.x (Green Tea GC default, go fix modernizers - verify patch at build), chi (latest - verify at build), gin (latest - verify at build), echo (latest - verify at build), google.golang.org/grpc (latest - verify at build), sigs.k8s.io/controller-runtime (latest - verify at build), kubebuilder (latest - verify at build), tinygo (latest - verify board targets at build), wazero (latest - verify at build), and golangci-lint (latest - verify at build).
Reviewed by Chris St. John·Last updated Jul 16, 2026