Functions, Methods & Interfaces Best Practices
Idiomatic rules for designing functions, methods, and interfaces that stay readable under concurrency, testing, and years of module upgrades.
How to Use This List
- Apply during API review for new packages and before stabilizing
/v2module paths. - Cross-check handler/repository boundaries in HTTP and gRPC services against sections A–D.
- Pair with
golangci-lint(ireturn,revive,govet) so conventions become CI signal. - Revisit when upgrading Go toolchains -
go fixmodernizers in 1.26 may shift idioms.
A - Functions and Returns
- Return
(T, error)witherrorlast. Matches reader expectations and staticcheck conventions. - Wrap errors with
%wand domain prefixes.fmt.Errorf("checkout: charge: %w", err)preserveserrors.Ischains. - Avoid naked returns except short defer/error helpers. Explicit
returnvalues scan faster in review. - Prefer functional options over growing parameter lists.
New(WithPort(8080))scales without breaking callers. - Pass
context.Contextfirst on I/O boundaries. Never store context in structs; thread it through parameters.
B - Methods and Receivers
- Pick one receiver style per type. If any method mutates, default to pointer receivers for all methods on that type.
- Constructors return
*Twhen pointer methods exist. Ensures interface satisfaction without address surprises. - Keep value receivers for small immutable types. IDs, money types, or read-only views where copying is cheap and safe.
- Do not copy structs containing locks.
sync.Mutex,sync.WaitGroup, and similar fields require pointer receivers only. - Name constructors
NeworNew<Type>. Document required dependencies; validate inputs before returning.
C - Interfaces and Composition
- Define interfaces at consumers with 1–3 methods.
io-style small surfaces beat enterprise-wideRepositoryinterfaces. - Accept interfaces, return concrete structs. Parameters depend on behavior;
Newreturns*Service, notServiceInterface. - Avoid exporting wide interfaces from producers. Adding a method breaks every external implementer.
- Document nil semantics for interface returns. Prefer
(*T, error)or untyped nil interfaces - never typed nil pointers in interfaces. - Use function types for single hooks. Middleware
func(http.Handler) http.Handlerstays simpler than one-method interfaces when no mock needed.
D - Closures, Variadics, and Concurrency
- Copy loop variables before goroutine closures.
id := idor pass parameters togo func(id int){...}(id). - Guard nil function values before call. Optional callbacks should no-op when unset.
- Treat variadic args as
len==0checks, not nil slice identity. Zero-arg calls pass empty non-nil slices. - Limit closure capture to needed fields. Prevent retaining large request objects in long-lived handlers.
- Run
go test -raceon packages using goroutines with captured state. Philosophy without detection fails in production.
Applying the List in Review
- Blocking: typed nil returned as interface, mixed receiver styles breaking interfaces, missing context on RPC/HTTP I/O, goroutine loop capture bugs.
- Discuss: splitting interfaces, converting func hooks to interfaces for testing, adding variadic options vs config struct.
- Defer: renaming functions for aesthetics when signatures are stable and exported.
FAQs
Which section should new services prioritize first?
Start with A and C for public APIs, then B for domain types, then D when adding concurrency.
Most production bugs trace to interfaces and errors, not variadic syntax.
Should every repository be an interface?
In services with tests, yes - define the interface beside the consumer.
Single-binary tools may defer until a second implementation or mock appears.
How do chi/gin/echo handlers fit these rules?
Keep framework types at the edges.
Call domain functions returning (T, error) and map errors to HTTP status inside handlers.
When is returning an interface from New acceptable?
Rarely - when the concrete type must stay sealed and the interface is the product (plugins, driver factories).
Document evolution policy.
What is the top mistake with named returns?
Shadowing err with := so defer no longer updates the named result.
Use assignment on the outer err or avoid named results entirely.
How small should interfaces be for gRPC adapters?
Wrap generated clients behind one- or two-method interfaces per use case.
Do not mirror entire gRPC service interfaces in domain code.
Should I lint for functions returning interfaces?
ireturn can flag returns of interfaces from functions - tune severity to match "return structs" policy.
Consistency matters more than enabling every analyzer.
How do generics change interface guidance?
Use generics for shared algorithms; keep interfaces for runtime substitution and I/O.
Do not recreate Java-style generic repository hierarchies.
Are variadic functional options always better than Config structs?
Options excel for optional tuning.
Required, validated configuration with many interdependent fields fits a struct checked once in New.
How does this list interact with kubebuilder scaffolds?
Generated reconcilers already use pointer receivers and injected clients.
Apply section C when abstracting cloud SDKs behind domain interfaces.
What about TinyGo or WASM targets?
Closures and interfaces still apply; avoid capturing large state when memory is tight.
Verify interface calls on hardware targets at build time.
How often should teams revisit the list?
During major Go upgrades and quarterly API reviews.
Add a failing regression test when a typed-nil bug ships - link it in the team wiki.
Related
- Functions and Interfaces: Go's Composition Model - conceptual foundation for section C
- Defining and Implementing Interfaces - consumer-side interface patterns
- Nil Interface vs Nil Pointer - avoid typed-nil interface returns
- Methods: Value vs Pointer Receivers - receiver consistency details
- Function Literals & Closures - safe capture and middleware patterns
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 linter set at build).