Generics
Constraints, inference, and when not to reach for them.
Generics landed in Go and the community promptly split into "finally!" and "please don't." Both reactions are right, depending on what you do with them. Used well, generics remove a whole category of interface{} boilerplate and unsafe type assertions. Used carelessly, they make code harder to read for no benefit.
This post is about the good uses, the constraints system, and — just as important — when to leave generics in the drawer.
The problem they solve
Before generics, a reusable container or utility meant interface{} (now any) and runtime type assertions:
// Pre-generics: loses type safety, requires assertions, can panic.
func First(s []any) any {
return s[0]
}
x := First([]any{1, 2, 3}) // x is `any` — caller must assert x.(int)You lost compile-time type checking and paid for boxing. Generics give you the reuse and the type safety:
func First[T any](s []T) T {
return s[0]
}
x := First([]int{1, 2, 3}) // x is int, checked at compile time
y := First([]string{"a"}) // y is string[T any] declares a type parameter. The compiler infers T from the argument, so you rarely write it explicitly. No assertions, no boxing, full type safety.
Constraints: limiting what T can be
any means "any type," which only lets you do things every type supports (assign, pass around). To actually operate on a T — add it, compare it — you constrain it to types that support those operations.
A constraint is an interface that may also list concrete types:
// Numeric is satisfied by any of these underlying types.
type Numeric interface {
~int | ~int64 | ~float64
}
func Sum[T Numeric](nums []T) T {
var total T
for _, n := range nums {
total += n // legal: every type in Numeric supports +
}
return total
}The ~ means "any type whose underlying type is this," so a type Celsius float64 also satisfies ~float64. Without the ~, only the exact type float64 would qualify.
The standard library ships ready-made constraints in constraints and, most usefully, comparable (built in) for anything usable as a map key or with ==:
// comparable is a built-in constraint: types that support == and !=.
func Contains[T comparable](s []T, target T) bool {
for _, v := range s {
if v == target {
return true
}
}
return false
}For ordering, cmp.Ordered (from the standard cmp package) covers everything that supports <, >, etc.:
func Max[T cmp.Ordered](a, b T) T {
if a > b {
return a
}
return b
}(Note: min and max are now built-in for scalars; write your own generic version only when you need it over custom orderings or slices.)
Where generics genuinely shine
1. Collection / slice / map utilities. The poster child. The standard slices and maps packages are built on generics:
func Map[T, U any](s []T, f func(T) U) []U {
result := make([]U, len(s))
for i, v := range s {
result[i] = f(v)
}
return result
}
func Filter[T any](s []T, keep func(T) bool) []T {
var result []T
for _, v := range s {
if keep(v) {
result = append(result, v)
}
}
return result
}
names := Map([]User{...}, func(u User) string { return u.Name })
adults := Filter(users, func(u User) bool { return u.Age >= 18 })2. Type-safe containers. A generic stack, set, ordered cache, or tree eliminates per-type duplication or any-based versions:
type Stack[T any] struct {
items []T
}
func (s *Stack[T]) Push(v T) { s.items = append(s.items, v) }
func (s *Stack[T]) Pop() (T, bool) {
var zero T
if len(s.items) == 0 {
return zero, false
}
v := s.items[len(s.items)-1]
s.items = s.items[:len(s.items)-1]
return v, true
}Note var zero T — the idiomatic way to produce the zero value of a type parameter when you have nothing to return.
3. Removing any from data-structure-ish code. Anywhere you were reaching for any plus a type assertion to write something reusable, a type parameter is almost always clearer and safer.
When NOT to use generics
This is the part people skip, so read it twice. Generics are a tool for removing duplication across types. If that's not your problem, they add complexity for nothing.
- An interface already expresses what you need. If you only call methods on a value, an interface is simpler and more idiomatic.
io.Readerdoesn't need to be generic. Reach for generics when you need to operate on a value generically (compare it, do arithmetic, store it in a typed container) — not just call its methods. - There's only one type in practice. Don't make a function generic "in case" you need another type later. Add the type parameter when the second type actually shows up. A concrete function is easier to read.
- It hurts readability. A signature like
func F[T any, U comparable, V cmp.Ordered](...)with three constraints is a sign you've gone too far. If a reader has to decode the type machinery to understand what the function does, reconsider. - For methods that vary by type. Go does not allow type parameters on individual methods (only on the type). If you need polymorphic behavior per type, that's what interfaces are for.
A useful heuristic: interfaces are about behavior; generics are about types. If you're abstracting over "things that can do X," use an interface. If you're abstracting over "a container/algorithm that works for any type," use generics.
Interfaces for behavior, generics for types. Don't make something generic until a second type actually demands it — and never trade readability for abstraction you don't yet need.
A quick note on performance
Don't choose generics for performance, and don't avoid them for performance — the effect is usually negligible and implementation-dependent. Choose them for type safety and reduced duplication. If a hot path's allocations matter, profile it (post 8) rather than guessing about generic vs. interface dispatch.
The takeaway
Generics let you write reusable, type-safe code without interface{} and runtime assertions — ideal for slice/map utilities and typed containers. Constrain your type parameters to exactly the operations you need (comparable, cmp.Ordered, or a custom ~type union), and use var zero T for zero values. But remember the dividing line: interfaces for behavior, generics for types. Don't make something generic until a second type actually demands it, and never trade readability for abstraction you don't yet need.