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.
Search across all documentation pages
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.
T) stands in for a concrete type at compile time, and a constraint interface limits which types may replace T.interface{} casts, go generate boilerplate, and copy-paste helpers for slices, maps, and containers without giving up static typing.interface{} or codegen.slices/maps/cmp stdlib packages, reflection.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.
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 |
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.
any means I can do anything with T - You may store any value, but you still cannot use == on T unless the constraint is comparable, and you cannot call methods not listed on the constraint.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.
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.
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.
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.
Map keys, some sync primitives, and any algorithm using == on T need comparable.
Slices, maps, and functions are not comparable in Go.
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.
Inference works when argument types pin each parameter.
When it fails, pass explicit type arguments: Convert[int](v).
A constraint may require methods; concrete types satisfy implicitly.
A type parameter constrained to io.Reader accepts any reader implementation without inheritance.
Go 1.21 added generic slices, maps, and cmp packages.
Prefer them before custom copies.
Generics Basics for snippets, then Type Parameters & Constraint Interfaces for ~ and unions.
Conceptual content applies to Go 1.18+ generics.
Stdlib examples assume Go 1.21+ for slices/maps/cmp.
The designers prioritized simplicity, fast compiles, and readable errors over C++-level metaprogramming power.
Constraints keep generic APIs grep-friendly and reviewable.
~ and union syntaxStack 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).
Reviewed by Chris St. John·Last updated Jul 19, 2026