Go Generics in Production: What We Use, What We Avoid, and Why
Go generics shipped in 1.18. Two years in, our codebase has adopted them in specific places and rejected them in others. An honest usage report.

Go generics landed in 1.18 (March 2022). Two years later our monorepo (~840k LOC Go, 1.22 toolchain) uses generics in specific, boring places and explicitly avoids them in others. This is a usage report, not a style guide for hypotheticals.
Where we use generics (and should keep using them)
Container types with compile-time type safety
Before: interface{} maps or code generation with go generate stringer patterns.
After: small internal package containers:
type Set[T comparable] struct {
m map[T]struct{}
}
func NewSet[T comparable]() *Set[T] { ... }
Used in 7 services for deduplication (IDs, connection keys). Eliminated a class of type assertion panics. No performance regression vs hand-rolled map[string]struct{} — compiler monomorphizes reasonably.
Generic retry / polling helpers
func Poll[T any](ctx context.Context, interval time.Duration, fn func() (T, error)) (T, error)
4 call sites. Previously copy-pasted with interface{} return + cast. Generics removed casts; errors unchanged.
Test fixtures: optional pointers
Pattern from community, adopted internally:
func Ptr[T any](v T) *T { return &v }
Trivial. Saves new(int); *x = 5 noise in table tests. 200+ uses in _test.go files only — zero production hot path.
Ordered constraints for min/max/clamp
func Clamp[T constraints.Ordered](v, lo, hi T) T { ... }
Shared mathx package. Replaced three float64/int64/_duration copies. Lint rule: production use OK; don't expose in public API modules consumed by external teams without semver bump discussion.
Where we rejected generics
HTTP handlers and middleware chains
Attempted generic middleware func Wrap[T ReqBody](handler func(T) error) http.Handler — abandoned. Error messages opaque; go vet and stack traces harder to read for mid-level Go devs on team. Standard http.Handler + typed handlers at registration site won.
Database repository layer
Proposal: Repository[T Entity] with CRUD methods. Rejected after spike:
- Every entity needs different queries, joins, soft-delete semantics
- Interface grew to 15 methods; mock generation painful
- Junior devs couldn't navigate generic constraints + sqlx tags
Stuck with explicit per-entity repos. Boring. Shippable.
JSON API response wrappers
type Response[T any] struct { Data T; Error string } — tempted for OpenAPI consistency. Rejected: OpenAPI codegen (ogen) already generates per-type structs; generic wrapper confused JSON schema for polymorphic endpoints.
Sync.Pool equivalents
Generic Pool[T] wrapper — micro-benchmark showed no win over typed pools for []byte and *bufio.Reader separately. Allocator behavior clearer with two explicit pools.
Performance notes (honest)
Micro-benchmarks on M1 / Linux amd64, Go 1.22:
| Pattern | Generic vs non-generic |
|---|---|
| Set insert/lookup | Within noise (~1%) |
| Clamp int64 | Identical |
| Poll helper | Identical (inlined) |
Binary size: +2.3 MB total across 23 binaries vs pre-generics codegen approach — acceptable. One service with heavy Set[ComplexStruct] monomorphization added 400 KB — watch if embedding large structs.
Team adoption friction
- Readability split: senior engineers prefer generics for containers; some staff engineers report "can't skim type errors"
- Code review rule: generics require comment on constraint choice if not
comparableor standard library constraint - Onboarding: added 30-min module to internal Go course — "generics for helpers, not architecture"
Cross-ref: Python type annotations at scale — similar "types at boundaries" philosophy. API style elsewhere: gRPC vs REST choices.
golangci-lint interaction
gocritic sometimes suggests generics where duplication is intentional (similar but domain-different structs). Disabled rule whyNotMerge locally — bad name, our tag.
What we'd try next (maybe)
slices/mapsstdlib — migrate hand-rolled helpers where 1.21+ packages suffice (generics already inside stdlib)- Generic event bus — third attempt blocked until we have 3 identical consumer patterns (rule of three)
- Evaluate Go 1.23 range-over-func iterators for pagination — may reduce generic iterator need
Decision rubric (current)
Use generics when:
- Same algorithm, 2+ types, no domain divergence
- Type safety removes casts or codegen
- API is internal or small stable surface
Avoid when:
- Behavior diverges per type beyond data layout
- Stack traces and errors are primary debug path for on-call
- OpenAPI/JSON schema generation involved
Go generics are a scalpel in our codebase, not a architecture hammer. That was the right expectation setting in 1.18 — still true in 1.22.
Compiler and IDE ergonomics
Go 1.22 + gopls 0.15: hover on generic instantiations shows monomorphized type — helped adoption. Constraint violation errors still intimidate junior devs — kept FAQ with common fixes.
Public modules that exported Paginate[T] forced semver major and go 1.18 minimum in go.mod — one external consumer on 1.17 couldn't build. Documented; acceptable trade.
Generics vs code generation history
Before 1.18 we used go generate with string templates for type-safe sets — 400 lines generator, 1200 lines generated. Deleted generator; net -800 LOC. Maintenance win larger than runtime win.
counterfeiter and mockgen still used for interfaces — generics didn't replace interface mocking patterns.
What we'd try next (maybe)
Manish Bookreader
Electronics enthusiast, Embedded Systems Expert, Linux/Networking programmer, and Software Engineer passionate about AI, electronics, books, and cooking.

