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.
[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 float64A 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.
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 = truecomparable 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.
$ go run generics.go
[a b]
6