Go Generics Cheat Sheet
Explains Go's generic type parameters, type constraints, the built-in constraints package, and how to write reusable generic functions and data structures.
Generic Functions
Declaring a function with a type parameter.
// T is constrained to types supporting comparison operatorsfunc Max[T int | float64 | string](a, b T) T { if a > b { return a } return b}func main() { fmt.Println(Max(3, 7)) // 7 (T inferred as int) fmt.Println(Max(2.5, 1.1)) // 2.5 (T inferred as float64) fmt.Println(Max("go", "rust")) // "rust"}
Custom Type Constraints
Defining reusable constraint interfaces.
type Number interface { ~int | ~int32 | ~int64 | ~float32 | ~float64}// ~int allows any type whose underlying type is int (e.g. type MyInt int)func Sum[T Number](nums []T) T { var total T for _, n := range nums { total += n } return total}// comparable permits == and !=func Contains[T comparable](s []T, target T) bool { for _, v := range s { if v == target { return true } } return false}
Constraint Keywords
Syntax used when writing type constraints.
- any- Alias for interface{}; permits any type, no operations besides assignment
- comparable- Built-in constraint allowing == and !=; required for map keys
- ~T (tilde)- Matches T and any type whose underlying type is T (approximation element)
- | (union)- Combines multiple types/terms into one constraint, e.g. int | float64
- constraints.Ordered- From golang.org/x/exp/constraints; types supporting <, <=, >, >=
- type set- A constraint interface defines the set of permitted types, not just methods
Generic Types
A generic stack implementation using a type parameter.
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 } last := s.items[len(s.items)-1] s.items = s.items[:len(s.items)-1] return last, true}s := Stack[int]{}s.Push(1)s.Push(2)v, ok := s.Pop() // v == 2, ok == true
Built-in Generic Helpers
Generic functions available in modern Go without extra imports.
- min(a, b)- Built-in generic function (Go 1.21+) returning the smaller of two ordered values
- max(a, b)- Built-in generic function (Go 1.21+) returning the larger of two ordered values
- clear(m)- Built-in generic function that empties a map or zeroes a slice's elements
- slices.Sort(s)- Sorts a slice of any ordered type in place, from the slices package
- slices.Contains(s, v)- Reports whether v is present in slice s
- maps.Keys(m)- Returns an iterator over a map's keys, from the maps package
Generic Linked List
A singly linked list built with a self-referencing generic node type.
type node[T any] struct { val T next *node[T]}type List[T any] struct { head *node[T] len int}func (l *List[T]) Push(v T) { l.head = &node[T]{val: v, next: l.head} l.len++}func (l *List[T]) ForEach(fn func(T)) { for n := l.head; n != nil; n = n.next { fn(n.val) }}var l List[string]l.Push("a")l.Push("b")l.ForEach(func(s string) { fmt.Println(s) }) // "b", "a"
Map / Filter / Reduce with Two Type Params
Generic higher-order functions parameterized over an input and output type.
func Map[T, U any](s []T, fn func(T) U) []U { result := make([]U, len(s)) for i, v := range s { result[i] = fn(v) } return result}func Filter[T any](s []T, pred func(T) bool) []T { var out []T for _, v := range s { if pred(v) { out = append(out, v) } } return out}func Reduce[T, U any](s []T, init U, fn func(U, T) U) U { acc := init for _, v := range s { acc = fn(acc, v) } return acc}names := Map([]int{1, 2, 3}, func(n int) string { return fmt.Sprint(n) })evens := Filter([]int{1, 2, 3, 4}, func(n int) bool { return n%2 == 0 })sum := Reduce([]int{1, 2, 3}, 0, func(acc, n int) int { return acc + n })
Constraints That Require Methods
A type set constraint combined with a method set, and embedding one constraint in another.
type Stringer interface { String() string}// Combine a method requirement with comparable via embeddingtype ComparableStringer interface { comparable Stringer}func Dedup[T ComparableStringer](items []T) []T { seen := make(map[T]bool) var out []T for _, it := range items { if !seen[it] { seen[it] = true out = append(out, it) } } return out}// Note: you cannot embed a union element alongside methods in the// same interface literal used as both a type AND a constraint in// older Go versions -- constraint interfaces are compile-time only// and can't be used as ordinary variable types if they contain// non-interface type terms (e.g. `int | string`).
Generic Iterators (range-over-func)
Go 1.23+ iter.Seq lets a generic type expose a lazy, composable iterator usable directly in range.
import "iter"func (l *List[T]) All() iter.Seq[T] { return func(yield func(T) bool) { for n := l.head; n != nil; n = n.next { if !yield(n.val) { return // consumer stopped early (e.g. break) } } }}for v := range l.All() { fmt.Println(v) if v == "stop-here" { break // signals yield to return false and unwind cleanly }}
Generics Gotchas
Sharp edges that trip up developers moving beyond basic generic functions.
- No generic methods- A method cannot introduce its own new type parameters beyond the receiver's; only the receiver type can be generic
- Type inference stops at composite literals- Go often can't infer T from `[]T{}` or struct literals; pass an explicit type argument like Stack[int]{}
- No covariance- []Dog is not assignable to []Animal even if Dog implements Animal; generics don't add variance
- Zero value needs `var zero T`- You can't write `T(0)` or `nil` generically unless T is constrained to a pointer/interface-like set
- Monomorphization cost- The compiler generates specialized code per instantiated type combination (with dictionary-passing for interfaces), which can increase binary size
- Type sets are structural, not nominal- ~int matches any named type with underlying type int, so a constraint can silently admit more types than expected
- Constraint interfaces aren't ordinary types- An interface with type terms (unions/~) can only be used as a constraint, not as a variable's static type
Prefer defining the narrowest constraint you need (e.g. a custom Number interface) instead of `any` — it keeps compile-time type checking useful and avoids reflection-like escape hatches.