When to Avoid Generics in Go APIs
Not every exported function needs bracket syntax.
This cheatsheet lists situations where interfaces, concrete types, or small duplicates keep public APIs idiomatic and easier to evolve.
How to Use This List
- Scan tables before exporting a new generic symbol from a library or framework wrapper.
- Apply stricter rules to public packages; internal helpers can stay generic.
- Pair with Generics Best Practices for team checklist habits.
- Revisit after benchmarks - performance exceptions belong in code comments and ADRs.
Public API Signals
| Signal | Prefer instead | Example |
|---|---|---|
| Single concrete type in production | Non-generic function | ParseConfig(cfg Config) not Parse[T Config](T) |
| Behavior contract, many implementations | Small interface | io.Reader, Validator |
| HTTP/gRPC handler signature | Concrete request/response types | func(w http.ResponseWriter, r *http.Request) |
| Plugin or third-party extensibility | Interface + registration | Middleware chains in chi / gin / echo |
| JSON/dynamic schema | any + typed decode step | json.Unmarshal into struct |
Readability & Evolution
| Smell | Why avoid generics | Better shape |
|---|---|---|
| Brackets in every helper name | Harder grep and docs | Unexported sortItems[T] behind exported SortItems |
| Constraint with 8+ union members | Unstable, unreadable bound | Named domain type or non-generic split |
| Generic exported struct users never parameterize | API noise | Concrete struct or interface field |
| Changing constraint breaks callers silently at compile | Large blast radius | Stable interface surface, generic internals |
| Test doubles need many instantiations | Mock proliferation | Interface boundary |
Runtime & Binary Context
| Context | Generics risk | Guidance |
|---|---|---|
| WASM / tinygo | Many instantiations inflate binary | One code path or limited codegen |
| Hot path already uses interfaces for plugins | Marginal generic win | Benchmark before rewrite |
| Cross-module version skew | Instantiation symbols multiply | Keep generics in leaf packages |
| Reflection-based frameworks | Generics do not simplify reflection | Stay with interfaces |
Quick Decision Table
| Question | If yes | If no |
|---|---|---|
Will callers use more than one T? | Generic candidate | Concrete API |
| Is this exported? | Favor simpler surface | Internal generic OK |
Does T need methods from different domains? | Interface, not union | Narrow constraint |
| Is readability for junior devs a goal? | Avoid bracket-heavy exports | Internal use fine |
| Does stdlib already provide helper? | Use slices/maps/cmp | Custom generic |
Tiny Snippets - Interface vs Generic Export
| Goal | Avoid | Prefer |
|---|---|---|
| Sort users by age | func Sort[T any]([]T, func(T,T) bool) exported | slices.SortFunc(users, cmp.Compare) locally |
| Validate models | Validate[T Validator](T) with huge constraint | type Validator interface { Validate() error } |
| Repository get | Get[T any](id string) (T, error) public | GetUser(id string) (User, error) |
FAQs
Should standard library style guide my exports?
Yes - stdlib keeps public APIs mostly non-generic and uses generics inside slices, maps, and cmp.
Follow that split.
Are generic HTTP handlers idiomatic?
Rarely - frameworks expose concrete contexts and interfaces.
Keep generics in shared internal utilities.
What about generic repositories?
Get[T] tempts but spreads bracket syntax and tightens constraints on models.
Prefer domain methods per aggregate.
Can I export constraints?
Only when downstream packages must define compatible generics.
Otherwise keep constraints unexported.
When are generics in gRPC code OK?
Internal batch helpers and test utilities - not .proto-generated surface.
Generated code stays concrete.
Does avoiding generics mean avoiding type safety?
No - concrete types and interfaces are statically checked.
Generics are one tool, not the only safe tool.
How does this interact with kubebuilder controllers?
Reconcilers stay interface-driven against client-go types.
Use generics in small list/dedupe helpers inside the project.
Should open-source libraries lead with generics?
Lead with simple interfaces and concrete types.
Add generics later without breaking surface when possible.
What is the internal/generic split?
Exported: Process(items []Item).
Unexported: dedupe[T comparable]([]T) []T.
When is duplication better?
Two concrete functions with five lines each beat one generic with a ten-member union.
Related
- Generics Best Practices - team checklist
- Performance Implications of Generics - when generics help performance
- Generics in Go: Constraints Without Templates - what generics are for
- Type Parameters & Constraint Interfaces - constraint complexity
- Generic Algorithms: slices, maps & cmp Packages - stdlib first
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).