Generics
Go added type parameters — generics — in Go 1.18 (March 2022). The mechanism admits writing functions and types parameterised by one or more types, with constraints specifying what operations the parameters support. The Go generics design is deliberately minimal compared with C++ templates or Rust generics: no specialisation, no variance, no type-class hierarchies. The principal benefit is the elimination of the conventional pre-1.18 patterns — interface{} with type assertions, code generation via go generate, separate functions for []int, []string, etc. The Go 1.21+ slices, maps, and cmp packages are the conventional generic library surface.
Generic functions
Type parameters appear in square brackets after the function name:
func Max[T int | float64](a, b T) T {
if a > b {
return a
}
return b
}
x := Max(3, 5) // T inferred as int
y := Max(3.0, 5.0) // T inferred as float64
z := Max[float64](3, 5) // explicit; treats both as float64
The form: func Name[TypeParams](params) returnType { body }. Each type parameter has a constraint — an interface specifying what operations the parameter supports. The constraint int | float64 admits values of either int or float64.
Constraints
A constraint is an interface; type parameters require a value to satisfy the interface. The constraint may include:
- Method requirements — like ordinary interfaces.
- Type sets — explicit types or unions (
int | float64 | string). - Approximation —
~intadmits any type whose underlying type isint.
type Numeric interface {
int | int32 | int64 | float32 | float64
}
func Sum[T Numeric](s []T) T {
var sum T
for _, v := range s {
sum += v
}
return sum
}
fmt.Println(Sum([]int{1, 2, 3})) // 6
fmt.Println(Sum([]float64{1.1, 2.2})) // 3.3
The any constraint
any is an alias for interface{} — it admits any type:
func First[T any](s []T) T {
return s[0]
}
func Print[T any](v T) {
fmt.Println(v)
}
The any constraint admits no operations beyond what interface{} admits — assignment and fmt.Print-style formatting. For specific operations, more restrictive constraints are needed.
The comparable constraint
comparable admits types that support == and !=:
func Index[T comparable](s []T, target T) int {
for i, v := range s {
if v == target {
return i
}
}
return -1
}
idx := Index([]int{1, 2, 3}, 2) // 1
idx := Index([]string{"a", "b"}, "a") // 0
comparable admits all the basic types (numbers, strings, bools), pointers, interfaces, channels, and structs whose fields are all comparable. Slices, maps, and functions are not comparable.
The constraints package (legacy)
The golang.org/x/exp/constraints package provides several pre-defined constraints; since Go 1.21, cmp.Ordered is the conventional choice for ordered types:
import "cmp"
func Max[T cmp.Ordered](a, b T) T {
if a > b {
return a
}
return b
}
cmp.Ordered admits int, float64, string, and similar types that support <, <=, >, >=.
Approximation ~T
The ~T form in a constraint admits any type whose underlying type is T:
type MyInt int // underlying type is int
type Numeric interface {
int | float64 // exact match
}
type LooseNumeric interface {
~int | ~float64 // any type with int or float64 underlying
}
// LooseNumeric admits MyInt; Numeric does not.
func Sum[T LooseNumeric](s []T) T {
var sum T
for _, v := range s {
sum += v
}
return sum
}
vs := []MyInt{1, 2, 3}
fmt.Println(Sum(vs)) // 6
The ~ form is conventional for generic functions that should work on user-defined types.
Generic types
Type parameters appear in square brackets after the type name:
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) {
if len(s.items) == 0 {
var zero T
return zero, false
}
n := len(s.items) - 1
v := s.items[n]
s.items = s.items[:n]
return v, true
}
s := &Stack[int]{}
s.Push(1)
s.Push(2)
v, _ := s.Pop() // v is int, value 2
The Stack[int] is the instantiation; Stack[T] is the generic type. Each instantiation is a distinct concrete type at compile time.
Generic methods
Methods on a generic type may not introduce new type parameters; they share the type’s parameters:
type Stack[T any] struct { /* ... */ }
func (s *Stack[T]) Push(v T) { /* ... */ } // OK
func (s *Stack[T]) Pop() (T, bool) { /* ... */ } // OK
// func (s *Stack[T]) Map[U any](f func(T) U) *Stack[U] // NOT admitted in Go
The restriction is a deliberate design decision; for methods that need additional type parameters, top-level generic functions are the conventional substitute.
Type inference
The compiler infers type parameters when they can be determined from the arguments:
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
}
// T inferred as int; U inferred as string:
strings := Map([]int{1, 2, 3}, func(n int) string {
return fmt.Sprintf("%d", n)
})
// Explicit:
strings := Map[int, string]([]int{1, 2, 3}, func(n int) string { /* ... */ })
When the type cannot be inferred, the explicit form is required.
The standard library generic packages
Since Go 1.21, the standard library provides generic packages:
slices
import "slices"
slices.Sort(s) // sort in place (cmp.Ordered)
slices.SortFunc(people, func(a, b Person) int {
return cmp.Compare(a.Age, b.Age)
})
slices.Index(s, target) // first index of target
slices.IndexFunc(s, func(v int) bool { return v > 10 })
slices.Contains(s, target)
slices.ContainsFunc(s, predicate)
slices.Equal(a, b)
slices.EqualFunc(a, b, eqFn)
slices.Reverse(s)
slices.Clone(s)
slices.Insert(s, index, values...)
slices.Delete(s, i, j)
slices.Min(s) // (cmp.Ordered)
slices.Max(s)
maps
import "maps"
maps.Clone(m)
maps.Copy(dst, src)
maps.Equal(a, b)
maps.DeleteFunc(m, func(k string, v int) bool { return v < 0 })
// Iterators (Go 1.23+):
for k, v := range maps.All(m) { /* ... */ }
for k := range maps.Keys(m) { /* ... */ }
for v := range maps.Values(m) { /* ... */ }
cmp
import "cmp"
cmp.Compare(a, b) // -1, 0, or 1
cmp.Less(a, b) // true if a < b
// Constraint:
type Ordered = cmp.Ordered
iter (Go 1.23+)
The iterator protocol:
import "iter"
type Seq[V any] func(yield func(V) bool)
type Seq2[K, V any] func(yield func(K, V) bool)
func Evens(max int) iter.Seq[int] {
return func(yield func(int) bool) {
for i := 0; i < max; i += 2 {
if !yield(i) { return }
}
}
}
for n := range Evens(10) {
fmt.Println(n)
}
The mechanism admits user-defined iterators; the range form admits iterating with structured iteration.
Common patterns
Generic min/max/sum
import "cmp"
func Min[T cmp.Ordered](a, b T) T {
if a < b { return a }
return b
}
func Max[T cmp.Ordered](a, b T) T {
if a > b { return a }
return b
}
type Numeric interface {
~int | ~int32 | ~int64 | ~float32 | ~float64
}
func Sum[T Numeric](s []T) T {
var total T
for _, v := range s {
total += v
}
return total
}
Map and filter
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, pred func(T) bool) []T {
result := s[:0] // reuse backing array
for _, v := range s {
if pred(v) {
result = append(result, v)
}
}
return result
}
Generic container types
type LinkedList[T any] struct {
head *node[T]
}
type node[T any] struct {
value T
next *node[T]
}
func (l *LinkedList[T]) PushFront(v T) {
l.head = &node[T]{value: v, next: l.head}
}
func (l *LinkedList[T]) Each(f func(T)) {
for n := l.head; n != nil; n = n.next {
f(n.value)
}
}
Generic comparison
func Equal[T comparable](a, b T) bool {
return a == b
}
func IndexOf[T comparable](s []T, target T) int {
for i, v := range s {
if v == target {
return i
}
}
return -1
}
Constraint composition
type Number interface {
~int | ~int32 | ~int64 | ~float32 | ~float64
}
type Comparable interface {
Number
~string
}
func Compare[T Number](a, b T) int {
if a < b { return -1 }
if a > b { return 1 }
return 0
}
Generic worker pool
type Pool[T any] struct {
work chan T
wg sync.WaitGroup
}
func New[T any](workers int, fn func(T)) *Pool[T] {
p := &Pool[T]{
work: make(chan T),
}
for i := 0; i < workers; i++ {
p.wg.Add(1)
go func() {
defer p.wg.Done()
for item := range p.work {
fn(item)
}
}()
}
return p
}
func (p *Pool[T]) Submit(item T) {
p.work <- item
}
func (p *Pool[T]) Close() {
close(p.work)
p.wg.Wait()
}
A note on what generics admit
Go’s generics are deliberately minimal:
- Type parameters on functions and types — admitted.
- Approximation constraints (
~T) — admitted. - Constraint composition — admitted via interface embedding.
- Method-level type parameters — not admitted.
- Specialisation — not admitted (no per-type implementations).
- Variance (covariance, contravariance) — not admitted.
- Type-level computation — not admitted (no
ifon types, no associated types). - Higher-kinded types — not admitted (no
F[T]whereFis itself generic).
The discipline produces a substantially simpler generics system than C++ or Haskell at the cost of some expressiveness.
A note on the conventional discipline
The contemporary Go generics advice:
- Reach for generics when the function would otherwise need
interface{}and type assertions. - Prefer non-generic code when one or two concrete types would suffice — generics adds complexity.
- Use
cmp.Orderedfor ordered comparisons. - Use
comparablefor equality. - Use
~Tin constraints to admit user-defined types. - Use the standard
slices,maps,cmppackages — they are the conventional generic library. - Type inference is good — explicit type parameters are rarely needed.
The combination — type parameters with constraints, structural type sets via interfaces, the standard generic library, no method-level parameters or specialisation — is the substance of Go’s generics. The discipline trades expressiveness for simplicity; the mechanism admits substantial reuse without the substantial complexity of richer generics systems.