Go Generics: How to Write Type Parameters, Constraints, and Generic Interfaces
Write practical Go generics with type parameters, any, comparable, cmp.Ordered, unions, custom constraints, generic types, and interfaces.

Generics are useful when one implementation should perform the same operation for several types. In Go, you express that relationship with a type parameter and a constraint:
func Max[T cmp.Ordered](a, b T) T
Here, T is a type parameter and cmp.Ordered determines which type arguments are valid. The operation drives the constraint: use any when the implementation needs no type-specific operations, comparable for equality or map keys, and cmp.Ordered for built-in ordering.
What a type parameter actually is
A type parameter list follows a function or type name in square brackets. The Go introduction to generics describes instantiation as two compiler steps:
- Substitute each type argument for its type parameter.
- Verify that each type argument satisfies its constraint.
If the second step fails, instantiation fails and the program is invalid. For example, substituting time.Time into Max[T cmp.Ordered] does not work because cmp.Ordered is restricted to ordered strings and numbers.
Here is a complete example using both explicit instantiation and inference:
package main
import (
"cmp"
"fmt"
)
func Max[T cmp.Ordered](a, b T) T {
if a > b {
return a
}
return b
}
func Zero[T any]() T {
var zero T
return zero
}
func main() {
explicit := Max[int](3, 7)
inferred := Max("alpha", "omega")
zero := Zero[int]()
fmt.Println(explicit, inferred, zero)
}
Max[int](3, 7) supplies int explicitly. In the second call, the compiler infers string from the arguments. Zero has no arguments from which to infer T, so Zero() is insufficient; its type argument must be written explicitly.
Constraints are interfaces
Go constraints are interface types. In addition to describing required methods, a constraint interface can list concrete type terms and unions. The | operator means that either listed term is permitted:
package main
import "fmt"
type Number interface {
int64 | float64
}
func SumNumbers[K comparable, V Number](m map[K]V) V {
var sum V
for _, value := range m {
sum += value
}
return sum
}
func main() {
integers := map[string]int64{"a": 2, "b": 3}
floats := map[int]float64{1: 1.5, 2: 2.5}
fmt.Println(SumNumbers(integers))
fmt.Println(SumNumbers(floats))
}
V may be int64 or float64, so addition is valid. K must be comparable because it appears as a map key.
Without that constraint, even the generic function declaration is invalid:
// Invalid: K is not constrained to types permitted as map keys.
func BrokenKeys[K any, V any](m map[K]V) int {
return len(m)
}
The compiler rejects map[K]V because K is not known to satisfy comparable.
The three constraints you will use most
Choose the narrowest constraint required by the implementation:
anypermits arbitrary types. Use it when values are stored, returned, or passed around without type-specific operators.comparablepermits types supporting==and!=. This is also the required constraint for a map key.cmp.Orderedpermits ordered strings and numbers and enables<,<=,>, and>=. It was introduced in Go 1.21.
cmp.Ordered deliberately follows the built-in ordering operators. It therefore does not make struct values such as time.Time orderable. One alternative is to accept a comparison function, which works for arbitrary types. That design has costs for containers: they need an initialized function instead of having a useful zero value, and indirect comparison calls are harder for the compiler to inline.
Custom constraints and ~
A custom constraint can combine a type term with required methods. When the type term is based on a built-in type and the constraint also requires a method, use ~:
package main
import "fmt"
type LabeledInteger interface {
~int
Label() string
}
type Priority int
func (p Priority) Label() string {
return fmt.Sprintf("priority-%d", p)
}
func Describe[T LabeledInteger](value T) string {
return value.Label()
}
func main() {
fmt.Println(Describe(Priority(2)))
}
The ~int term admits a defined type whose underlying type is int, so Priority can satisfy the constraint and supply the required method. If the term were just int, the constraint could not be satisfied: built-in int has no methods, while a defined type with a method is not the exact type int.
For broadly reusable data structures, consider accepting a comparison function rather than requiring callers' types to have a particular method. Turning an existing method into a function is easier than adding a method to a type you do not control.
Generic types, not just functions
Types can have parameters and methods too. This tree uses comparable because Lookup checks values with ==:
package main
import "fmt"
type Tree[T comparable] struct {
Value T
Left, Right *Tree[T]
}
func (t *Tree[T]) Lookup(want T) *Tree[T] {
if t == nil {
return nil
}
if t.Value == want {
return t
}
if found := t.Left.Lookup(want); found != nil {
return found
}
return t.Right.Lookup(want)
}
func main() {
names := Tree[string]{
Value: "root",
Left: &Tree[string]{Value: "left"},
Right: &Tree[string]{Value: "right"},
}
fmt.Println(names.Lookup("right") != nil)
}
Tree[string] is an instantiated generic type. Compared with storing interface values, using T can permit more efficient storage, avoid type assertions, and provide build-time type checking.
Generic interfaces
Interfaces are types, so they can also take type parameters. The Go guidance on generic interfaces recommends giving an interface's own parameter the broad any constraint when possible. Concrete implementations can impose stronger requirements:
package main
import "fmt"
type Collection[T any] interface {
Add(T)
Contains(T) bool
}
type SliceSet[T comparable] struct {
values []T
}
func (s *SliceSet[T]) Add(value T) {
if !s.Contains(value) {
s.values = append(s.values, value)
}
}
func (s *SliceSet[T]) Contains(want T) bool {
for _, value := range s.values {
if value == want {
return true
}
}
return false
}
func main() {
var names Collection[string] = &SliceSet[string]{}
names.Add("gopher")
fmt.Println(names.Contains("gopher"))
}
Collection[T] itself does not require equality. SliceSet[T] tightens its parameter to comparable because its implementation uses ==.
When not to use generics
Start with the function, not an elaborate family of constraints. Add type parameters when it becomes clear that the same implementation should operate on several types.
Use an ordinary interface when implementations genuinely differ by type. A file and a random-number generator do not implement reading the same way, so separate methods behind an interface such as io.Reader are the appropriate model. Generics are strongest when the algorithm stays the same and only the participating types vary.





Comments
No comments yet.