Type parameters, constraints, and the comparable / any keywords.
Type Parameters
Before Go 1.18 you'd write a Max function for int, and another for float64,
and another for string, because the language couldn't express "any orderable
type". Generics fixed that.
A generic function or type has type parameters in square brackets after the name.
Generic functions
func Max[T int | float64 | string](a, b T) T {
if a > b { return a }
return b
}
The type parameter list is [T int | float64 | string]. T is the type
parameter; int | float64 | string is its constraint — the set of types
allowed for T.
You don't usually have to spell out the type — Go infers it from the arguments.
When inference doesn't work, write it explicitly: Max[int](3, 5).
Generic types
type Stack[T any] struct {
data []T
}
func (s *Stack[T]) Push(v T) { s.data = append(s.data, v) }
func (s *Stack[T]) Pop() T {
v := s.data[len(s.data)-1]
s.data = s.data[:len(s.data)-1]
return v
}
Stack[string] is a concrete type. You can have Stack[int], Stack[User],
whatever you like — each one is its own type.
When to reach for generics
Don't use generics just because you can. Good fits:
- Collections —
Set[T],Queue[T],LRU[K, V]. - Algorithms over slices and maps — sort, map, filter, find.
- Eliminating "T-and-also-T" function pairs —
MaxInt,MaxFloat→Max[T].
Bad fits:
- "Just in case it might be useful later" — concrete code is easier to read.
- When an interface would do — interfaces are simpler and don't need stamping.
- For polymorphic behavior — that's still what interfaces are for.
Go proverb: interface satisfies behavior, generics constrain types.