Generics Best Practices
Naming, constraint design, and gradual adoption in codebases.
These rules keep generic code readable in review, stable for callers, and aligned with stdlib patterns.
How to Use This List
- Tick items during PR review when a diff introduces type parameters.
- Start new packages with section A before exporting generic symbols.
- Use section D when upgrading Go toolchains or enabling golangci-lint on generic modules.
- Revisit quarterly as team familiarity grows - loosen only with benchmarks and ADRs.
A - Constraints and naming
- Name constraints when reused or exported.
type NodeID interface { ~int64 }beats repeating union lists. - Prefer
cmp.Orderedandcomparableover copied unions. Stay aligned with stdlib semantics. - Use
~Twhen named types should match. Celsius/Fahrenheit-style wrappers belong in the type set. - Keep union lists short (roughly three members) or split functions. Wide unions fail compile checks and reviews.
- Spell type parameters as single capital letters (
T,K,V) or domain names (ID,E). Avoid cute multi-letter abbreviations in public APIs.
B - API surface and exports
- Default exported APIs to concrete types or interfaces. Keep generics unexported helpers unless callers need parameterization.
- Do not export bracket-heavy function names without doc examples. Every exported generic needs a one-line instantiation sample.
- Avoid generic methods on non-generic types. Only generic types carry parameters into methods in Go.
- Stabilize constraints before v1. Changing a constraint is a breaking compile-time change for downstream generics.
- Mirror stdlib split: generic algorithms in
internal/, simple handlers outside. Matches chi / gin / echo service layout.
C - Algorithms and data structures
- Reach for
slices,maps, andcmpbefore custom copies. Maintain less, gain runtime updates. - Document shallow vs deep copy in map helpers.
maps.Clonealiases pointer values. - Use ring buffers or cap-bounded queues for long-lived FIFOs. Prevent slice dequeue memory leaks.
- Require
comparablefor map-keyed sets and dedupe. Do not fake slice keys. - Table-test multiple instantiations (
int,string, struct). One test function per behavior, several type args.
D - Performance, tooling, and rollout
- Benchmark interface vs generic before rewriting hot paths. Record results in PR or ADR.
- Watch binary size on WASM/TinyGo targets when adding instantiations. CI build those artifacts if you ship them.
- Run
go test -raceon generic containers used from goroutines. Mutex rules unchanged. - Enable golangci-lint on modules using generics. Catch redundant parameters and unused instantiations early.
- Adopt gradually: internal helpers first, export after second reuse. Avoid preemptive generic frameworks.
E - Documentation and review
- Link to constraint rationale in godoc. "K must be comparable because this function builds map[K]int."
- Call out when not to use the generic API. Point to interface-first alternative.
- Prefer examples in
Example*tests over long comment prose. Compiler checks examples. - Flag generic APIs in CHANGELOG as experimental until constraints stabilize.
- Onboard with Generics Basics before advanced constraint pages.
FAQs
How many type parameters are too many?
More than two or three on an exported symbol deserves scrutiny.
Compose helper types instead.
Should I genericize tests?
Subtests with multiple instantiations are fine.
Do not genericize testify/assert wrappers without value.
What belongs in godoc for a generic function?
Constraint meaning, example instantiation line, and performance note if non-obvious.
How do generics interact with kubebuilder codegen?
Keep generated API types concrete.
Use generics in hand-written utility packages only.
When should I delete a generic helper?
When stdlib adds equivalent function or only one instantiation remains.
Simpler concrete code wins.
Are type aliases with generics special?
Aliases do not create new generic definitions.
Define generic types on named struct/map types.
Should grpc middleware use generics?
Rarely in exported chains.
Internal metadata key helpers may use generics with comparable keys.
How do I review constraint changes?
Treat like API breaks - search repo for instantiations and run full CI compile matrix.
What lint rules help?
golangci-lint staticcheck and unused analyzers catch dead instantiations.
Pair with compile-all-variants CI job for libraries.
Can juniors maintain generic code?
Yes if exports stay small and constraints are named.
Over-generic public packages hurt onboarding - follow section B.
Related
- When to Avoid Generics in Go APIs - quick reference guardrails
- Generics in Go: Constraints Without Templates - section overview
- Type Parameters & Constraint Interfaces - syntax depth
- Performance Implications of Generics - benchmark guidance
- Generic Algorithms: slices, maps & cmp Packages - stdlib first rule
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).