Generics in Go: Constraints Without Templates
Go added type parameters in Go 1.18 so you can write functions and types that work across many concrete types while keeping compile-time type safety.
The design is deliberately smaller than template systems in C++ or Java.
Constraints are ordinary interface types (with type sets), not a separate template language.
This page is the conceptual anchor for the Generics section.
Generics Basics collects runnable snippets; sibling articles cover constraint syntax, comparable, data structures, stdlib helpers, and performance trade-offs.
Summary
- A type parameter (for example
T) stands in for a concrete type at compile time, and a constraint interface limits which types may replaceT. - Insight: Generics remove duplicated
interface{}casts,go generateboilerplate, and copy-paste helpers for slices, maps, and containers without giving up static typing. - Key Concepts: type parameter, constraint, type set, instantiation, monomorphization, comparable, any.
- When to Use: Shared algorithms (min, contains, merge), reusable containers (set, queue), and APIs where the caller should pick the element type without
interface{}or codegen. - Limitations/Trade-offs: No template specialization, no generic methods on ordinary types, larger binaries when many instantiations exist, and readability cost when overused in public APIs.
- Related Topics: interfaces, type assertions, code generation,
slices/maps/cmpstdlib packages, reflection.
Foundations
Before Go 1.18, reusable code often used interface{} (today any), small interfaces, or go generate to stamp out per-type copies.
Each approach traded safety or clarity for flexibility.
Generics add square-bracket type lists to functions and types: func Max[T cmp.Ordered](a, b T) T.
The compiler checks that every argument type satisfies the constraint before accepting the call.
A constraint is an interface type.
It can list methods (interface { String() string }), name types in a union (int | float64), or use the underlying type operator ~int (any type whose underlying type is int, including named integer types).
The predeclared constraints any (all types) and comparable (types allowed with == and !=) cover the most common cases.
Instantiation is explicit or inferred: Max[int](1, 2) or Max(1, 2) when the compiler can infer T.
Unlike C++ templates, Go has no macro expansion phase and no user-defined specialization hooks.
The compiler monomorphizes: it generates specialized machine code (or IR) per distinct instantiation, similar in spirit to stamping out MaxInt, MaxFloat64, and so on by hand.
Mechanics & Interactions
Type checking happens in two phases.
Definition time: the generic function body may only use operations permitted for every type in the constraint's type set.
If T is only int | float64, you cannot call T.String() unless the constraint includes that method.
Instantiation time: each call site substitutes concrete types.
If no valid substitution exists, compilation fails with a clear constraint error instead of a runtime panic from a bad type assertion.
// Constraint: Ordered types from cmp - int, float, string, etc.
func Min[T cmp.Ordered](a, b T) T {
if a < b {
return a
}
return b
}Type sets are the set of types a constraint accepts.
~byte | ~rune accepts both byte and custom named types with those underlying types.
Union elements must be comparable with each other when the constraint is used in contexts that require it (for example map keys need comparable).
Generics interact with interfaces cleanly: a type parameter constrained to an interface still uses Go's implicit interface satisfaction.
They do not introduce nominal inheritance.
Methods cannot declare their own type parameters in Go; only functions and named types at package level carry type parameter lists.
Methods on generic types use the type's parameters (func (s *Stack[T]) Push(v T)).
generic definition call site
------------------ ---------
func Map[F,T any](...) --> Map[string,int](...)
| |
v v
constraint check monomorphized
on F, T Map_string_int
| Approach | Strength | Weakness | Best Fit |
|---|---|---|---|
| Generics | Static typing, no casts | Syntax noise, binary size | Slice/map algorithms, containers |
| Small interfaces | Idiomatic Go APIs | Runtime dispatch, boilerplate | IO, plugins, behavior contracts |
any + type switch | Flexible parsing | Runtime errors, verbose | JSON adapters, debug printers |
go generate | Zero runtime cost, explicit types | Build-step complexity | High-performance hot paths with many types |
| Reflection | One implementation | Slow, no compile checks | Plugins, unknown schemas |
Advanced Considerations & Applications
Libraries like chi, gin, and echo rarely expose generic handler signatures; generics shine in internal utilities (pagination helpers, typed caches) and shared google.golang.org/grpc middleware utilities, not in every public route signature.
controller-runtime and kubebuilder projects often combine codegen (for CRDs) with generic list helpers in shared packages.
tinygo and wazero support varies by target; verify generic-heavy code on embedded and WASM builds in CI.
golangci-lint can flag redundant type parameters and over-generic APIs; pair generics with benchmarks when binary size matters (see Performance Implications).
Gradual adoption is idiomatic: start with unexported helpers, then widen APIs once constraints stabilize.
Prefer stdlib slices, maps, and cmp before maintaining custom copies.
When a constraint grows past three unions, extract a named constraint type (type Number interface { ~int \| ~float64 }) for readability.
Common Misconceptions
- "Generics replace interfaces in Go" - Interfaces model behavior and decouple packages; generics model type-safe reuse of data shapes. Most exported APIs still lead with interfaces.
- "Go generics work like C++ templates" - There is no specialization, no SFINAE, and no implicit conversions between instantiations. Errors are reported at compile time on the constraint, not after deep instantiation stacks.
anymeans I can do anything with T - You may store any value, but you still cannot use==onTunless the constraint iscomparable, and you cannot call methods not listed on the constraint.- "Monomorphization is always faster than interfaces" - Often yes for hot loops, but extra instantiations increase compile time and binary size; interfaces plus one allocation can win for cold paths.
- "I need generics on every helper" - Duplicating two concrete functions is sometimes clearer than a generic with a ten-type union constraint.
FAQs
What did Go add in Go 1.18 versus what stayed the same?
Go 1.18 added type parameters, constraint interfaces with type sets, and inference at call sites.
It did not add generic methods on non-generic types, template specialization, or operator overloading.
What is a type set in plain terms?
The type set is the collection of concrete types allowed to replace a type parameter.
Constraint interfaces define that set via methods, ~T underlying types, and | unions.
Why does Go use interfaces for constraints?
Interfaces were already Go's composition mechanism.
Reusing them for constraints keeps one notation for "what operations does T support?" instead of inventing a second type system.
What is monomorphization?
The compiler emits specialized code per distinct type argument (for example Stack[int] vs Stack[string]).
Each version is type-checked independently, like hand-written duplicates.
When is comparable required?
Map keys, some sync primitives, and any algorithm using == on T need comparable.
Slices, maps, and functions are not comparable in Go.
Can I use generics in method signatures?
Methods on a generic type use the type's parameters.
You cannot add a fresh type parameter list to an ordinary method on a non-generic type.
Does inference always work?
Inference works when argument types pin each parameter.
When it fails, pass explicit type arguments: Convert[int](v).
How do generics interact with interfaces?
A constraint may require methods; concrete types satisfy implicitly.
A type parameter constrained to io.Reader accepts any reader implementation without inheritance.
Are generics in the standard library?
Go 1.21 added generic slices, maps, and cmp packages.
Prefer them before custom copies.
What should I read next in this section?
Generics Basics for snippets, then Type Parameters & Constraint Interfaces for ~ and unions.
Is this page tied to a specific Go patch release?
Conceptual content applies to Go 1.18+ generics.
Stdlib examples assume Go 1.21+ for slices/maps/cmp.
Why did Go avoid a full template system?
The designers prioritized simplicity, fast compiles, and readable errors over C++-level metaprogramming power.
Constraints keep generic APIs grep-friendly and reviewable.
Related
- Generics Basics - runnable intro examples
- Type Parameters & Constraint Interfaces -
~and union syntax - Type Sets & the comparable Constraint - equality rules
- Generic Algorithms: slices, maps & cmp Packages - stdlib helpers
- When to Avoid Generics in Go APIs - API design guardrails
- Generics Best Practices - team adoption checklist
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).