HowtoGo
Home / Language / Generics
Language

Generics

Generics let a function or type work over any type that satisfies a constraint, written once instead of duplicated per type.

A generic function is an ordinary function with one extra piece, a pair of square brackets holding a stand-in type. Click any part of the signature to see what that part is.

The parts of a generic function signature

[T int | float64] declares a type parameter T constrained to int or float64. The union of types in brackets is the constraint; T stands in for whichever one is used at the call site.

func Sum[T int | float64](nums []T) T {
    var total T
    for _, n := range nums {
        total += n
    }
    return total
}

Go infers T from the argument's type, so Sum([]int{...}) doesn't need Sum[int]([]int{...}) spelled out. Explicit instantiation is only needed when inference can't determine the type on its own.

Sum([]int{1, 2, 3})       // T inferred as int
Sum([]float64{1.5, 2.5})  // T inferred as float64

A constraint is an interface listing the permitted types (or methods) for a type parameter. The compiler rejects any instantiation outside that set at compile time, not at runtime.

A type parameter constrained to a set of permitted types Sum[T Number] generic function T must be Number constraint int int64 float32 float64 e.g. Sum[float64] one instantiation

Naming a constraint as its own interface, like Number, keeps a function signature readable and lets multiple functions share the same constraint.

type Number interface {
    int | int64 | float32 | float64
}

func Sum[T Number](nums []T) T {
    var total T
    for _, n := range nums {
        total += n
    }
    return total
}

A generic type parameterizes a struct the same way: Stack[T any] holds a slice of T. Methods on a generic type repeat the type parameter in the receiver, (s *Stack[T]).

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
    }
    n := len(s.items) - 1
    v := s.items[n]
    s.items = s.items[:n]
    return v, true
}

Stack[string] instantiates the struct with T fixed to string. Every method call on s now works exclusively with strings, checked at compile time.

s := &Stack[string]{}
s.Push("a")
s.Push("b")
v, ok := s.Pop() // v = "b", ok = true

comparable is a built-in constraint satisfied by any type usable as a map key (with == and !=). It's why K can be a map key type here while V, constrained only by any, can be anything.

func Keys[K comparable, V any](m map[K]V) []K {
    keys := make([]K, 0, len(m))
    for k := range m {
        keys = append(keys, k)
    }
    return keys
}

any is an alias for interface, the loosest constraint: any type at all. It gives up compile-time type checks on the parameter itself, in exchange for accepting everything.

Terminal
$ go run generics.go
[a b]
6