Generics in Go: Type Parameters, Constraints and a Type-Safe Stack
Type parameters, any, comparable, union and ~ constraints, cmp.Ordered, a generic Stack type, and when to prefer interfaces instead.
On this page
Generics (added in Go 1.18) let you write one function or type that works with many types, while keeping full compile-time type safety. Before generics you had to duplicate code for every type or fall back to interface{} and lose type checking.
Type parameters
A generic function declares type parameters in square brackets. Each one has a constraint that says which types are allowed.
func PrintAll[T any](items []T) {
for i, item := range items {
fmt.Println(i, "->", item)
}
}
PrintAll([]string{"apple", "banana"})
PrintAll([]int{1, 2, 3})
PrintAll([]bool{true, false})The compiler infers T from the argument, so you rarely write PrintAll[int](...) explicitly.
Constraints: any, comparable and unions
any (alias of interface{}): any type at all. You can store and pass the values, but not use operators like + or == on them.comparable: types that support == and != (numbers, strings, booleans, pointers, channels, arrays and structs of comparable fields). Needed for map keys and equality checks.|.// Only int or string slices are allowed
func PrintStrInt[T int | string](items []T) {
for _, item := range items {
fmt.Println(item)
}
}
PrintStrInt([]int{1, 2})
// PrintStrInt([]bool{true}) // compile error: bool does not satisfy int | string
// comparable lets us use ==
func Index[T comparable](items []T, target T) int {
for i, v := range items {
if v == target {
return i
}
}
return -1
}
fmt.Println(Index([]string{"a", "b"}, "b")) // 1Reusable constraints and the ~ operator
Name a constraint as an interface to reuse it. The tilde ~ means "this type or any type whose underlying type is this type":
type Number interface {
~int | ~int64 | ~float64
}
func Sum[T Number](values []T) T {
var total T // zero value of T
for _, v := range values {
total += v // + is allowed: every type in Number supports it
}
return total
}
type Celsius float64
fmt.Println(Sum([]int{1, 2, 3})) // 6
fmt.Println(Sum([]float64{1.5, 2.5})) // 4
fmt.Println(Sum([]Celsius{10, 20.5})) // 30.5, works because of ~float64Without ~, Celsius would be rejected even though it is "just a float64". The standard library provides cmp.Ordered for every type that supports <:
import "cmp"
func Max[T cmp.Ordered](a, b T) T {
if a > b {
return a
}
return b
}
fmt.Println(Max(3, 7), Max("go", "rust")) // 7 rustGeneric types: a type-safe stack
Types can have type parameters too. The original repository declared a generic Stacks struct; here it is with methods, so it is actually useful:
type Stack[T any] struct {
elements []T
}
func (s *Stack[T]) Push(v T) {
s.elements = append(s.elements, v)
}
func (s *Stack[T]) Pop() (T, error) {
var zero T
if len(s.elements) == 0 {
return zero, errors.New("stack is empty")
}
last := s.elements[len(s.elements)-1]
s.elements = s.elements[:len(s.elements)-1]
return last, nil
}
func (s *Stack[T]) Len() int { return len(s.elements) }var tasks Stack[string]
tasks.Push("write tests")
tasks.Push("deploy")
next, _ := tasks.Pop()
fmt.Println(next) // deploy
var nums Stack[int]
nums.Push(42)
// nums.Push("x") // compile error: cannot use "x" as intNote var zero T: in generic code you cannot write 0 or "" because you do not know the type, so you declare a variable to get its zero value.
Generics in the standard library
You may not need to write generics often, because the standard library already uses them:
import (
"maps"
"slices"
)
nums := []int{3, 1, 2}
slices.Sort(nums) // [1 2 3]
fmt.Println(slices.Contains(nums, 2)) // true
fmt.Println(slices.Max(nums)) // 3
m := map[string]int{"b": 2, "a": 1}
keys := slices.Sorted(maps.Keys(m)) // [a b]When (not) to use generics
Use generics for data structures (stacks, caches, sets) and algorithms over collections (map, filter, sum, sort) where the logic is identical for every type.
Prefer interfaces when behaviour differs per type (for example, each payment gateway charges differently). If you find yourself writing a type switch inside a generic function, an interface is probably the better tool.
Key takeaways
[] and each has a constraint.any allows everything; comparable allows ==; unions (int | string) list types.~T includes custom types built on T; cmp.Ordered covers <.Stack[T] give compile-time type safety without duplication.Last updated on