Polyglot
Languages Go generics
Go § generics

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~int admits any type whose underlying type is int.
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 parametersnot admitted.
  • Specialisationnot admitted (no per-type implementations).
  • Variance (covariance, contravariance)not admitted.
  • Type-level computationnot admitted (no if on types, no associated types).
  • Higher-kinded typesnot admitted (no F[T] where F is 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.Ordered for ordered comparisons.
  • Use comparable for equality.
  • Use ~T in constraints to admit user-defined types.
  • Use the standard slices, maps, cmp packages — 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.