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.
-
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;
Newreturns*Concreteso 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
Newfor fresh values,Open/Dialfor resources, and pair withClose/Shutdownwhen cleanup is required. -
Document zero value policy: State clearly whether
var Tis usable (bytes.Buffer) or requiresNew()(Storewith 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.Contextas 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 intconstants instead of string keys to avoid collisions between middleware packages. -
Call next in middleware: Invoke
next.ServeHTTPunless the middleware fully wrote the response; document middleware order inmain. -
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 inmainor explicit constructors. -
Limit package globals: Prefer injecting
*sql.DB, loggers, and metrics registries frommaininstead ofDefault()singletons. -
Embed deliberately: Embed to promote methods on thin decorators (
http.Handler), not to simulate inheritance trees. -
Errors not panic: Return
errorfor validation and expected failures; reservepanicfor programmer bugs and startupMust*helpers. -
Map errors at the edge: Services return domain errors; HTTP/gRPC adapters translate with
errors.Isanderrors.As. -
Test with fakes: Implement consumer interfaces in
_test.goto 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.
FAQs
What is the single highest-leverage habit in this section?
Keep handlers thin and push rules into services behind small interfaces - most other practices support that boundary.
When should we adopt functional options team-wide?
When more than one exported constructor in your module needs optional settings that would otherwise break callers on every new field.
How do we stop god packages from returning?
Enforce import rules in review: domain packages must not import SQL drivers or HTTP routers; wire in cmd/ and internal/.
Are singletons ever acceptable?
Rarely in application code; prefer injection.
Library-level immutable lazy caches with sync.Once are the main exception.
How does this section relate to net/http?
Middleware and handler composition here apply directly to stdlib servers and chi; gin/echo follow the same ideas with different types.
Related
- Go Patterns Basics - command-oriented primer
- Go Idioms: Composition Over Inheritance - section overview
- Common Anti-Patterns in Go Codebases - smells to avoid
- Functional Options Pattern - configurable constructors
- Repository & Service Layer Patterns - layering playbook
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).