Generics let a function or type declare one or more type parameters in square brackets, each constrained by an interface that specifies which types are allowed. Before Go 1.18 (released March 2022), writing a function that worked across multiple types meant either duplicating code per type or falling back to interface{} with runtime type assertions; generics give compile-time type safety instead, without sacrificing Go’s single-implementation style.
A generic function
A type parameter list in square brackets follows the function name, naming a placeholder type and its constraint. The built-in constraint any (an alias for interface{}) allows every type; numeric constraints restrict to types that support operators like <.
func Max[T int | float64](a, b T) T {
if a > b {
return a
}
return b
}
fmt.Println(Max(3, 7)) // 7 — T inferred as int
fmt.Println(Max(2.5, 1.1)) // 2.5 — T inferred as float64Constraints: which types are allowed
A constraint is just an interface, optionally listing specific allowed types with |. The standard library's constraints package (and the built-in comparable constraint, for types usable with ==) provide common ones, and you can declare your own.
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
}
fmt.Println(Sum([]int{1, 2, 3})) // 6
fmt.Println(Sum([]float64{1.5, 2.5})) // 4.0Generic types, not just functions
Types can also take type parameters, which is how you'd write a single, reusable, type-safe container (a stack, a linked list, a set) instead of one implementation per type or falling back to interface{}.
type Stack[T any] struct {
items []T
}
func (s *Stack[T]) Push(item T) {
s.items = append(s.items, item)
}
func (s *Stack[T]) Pop() (T, bool) {
var zero T
if len(s.items) == 0 {
return zero, false
}
item := s.items[len(s.items)-1]
s.items = s.items[:len(s.items)-1]
return item, true
}
var s Stack[string] // a Stack that only ever holds strings
s.Push("a")