Generic Types and Constraints
Types can have type parameters too. A Stack[T], Set[T] or Cache[K, V] is written once and used with any element type, and the compiler keeps each use type-safe. This lesson covers declaring generic types, their methods, constraints on type parameters, constraints that refer to themselves, and generic type aliases.
A generic stack
package main import "fmt" 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 } v := s.items[len(s.items)-1] s.items = s.items[:len(s.items)-1] return v, true } func (s *Stack[T]) Len() int { return len(s.items) } func main() { var words Stack[string] words.Push("first") words.Push("second") w, _ := words.Pop() fmt.Println(w, words.Len()) nums := &Stack[int]{} nums.Push(42) n, ok := nums.Pop() _, ok2 := nums.Pop() fmt.Println(n, ok, ok2) }
second 1 42 true false
The type's parameters are declared once, after its name. Methods repeat the parameter names in the receiver — (s *Stack[T]) — and can use T throughout. A Stack[string] and a Stack[int] are different types; pushing an int onto a Stack[string] is a compile error. var zero T is the idiomatic way to get the zero value of a type parameter.
A generic set
Go has no built-in set, but a generic one is a few lines on top of a map. The element type must be comparable because it's used as a map key:
package main import ( "cmp" "fmt" "maps" "slices" ) type Set[T comparable] struct { m map[T]struct{} } func NewSet[T comparable](items ...T) *Set[T] { s := &Set[T]{m: make(map[T]struct{})} for _, it := range items { s.Add(it) } return s } func (s *Set[T]) Add(v T) { s.m[v] = struct{}{} } func (s *Set[T]) Has(v T) bool { _, ok := s.m[v]; return ok } func (s *Set[T]) Len() int { return len(s.m) } func (s *Set[T]) Intersect(other *Set[T]) *Set[T] { out := NewSet[T]() for v := range s.m { if other.Has(v) { out.Add(v) } } return out } // Sorted is a function, not a method, because it needs a stricter // constraint (cmp.Ordered) than the Set type itself (comparable). func Sorted[T cmp.Ordered](s *Set[T]) []T { return slices.Sorted(maps.Keys(s.m)) } func main() { a := NewSet("go", "rust", "zig") b := NewSet("python", "go", "zig") fmt.Println(a.Has("go"), a.Has("java"), a.Len()) fmt.Println(Sorted(a.Intersect(b))) }
true false 3 [go zig]
Methods can't add extra constraints to the type's own parameters, which is why Sorted is a plain function here.
Several type parameters
package main import "fmt" type Pair[K comparable, V any] struct { Key K Value V } func (p Pair[K, V]) String() string { return fmt.Sprintf("%v=%v", p.Key, p.Value) } type Cache[K comparable, V any] struct { data map[K]V limit int order []K // keys in insertion order, oldest first } func NewCache[K comparable, V any](limit int) *Cache[K, V] { return &Cache[K, V]{data: map[K]V{}, limit: limit} } func (c *Cache[K, V]) Put(k K, v V) { if _, exists := c.data[k]; !exists { if len(c.order) == c.limit { // full: evict the oldest delete(c.data, c.order[0]) c.order = c.order[1:] } c.order = append(c.order, k) } c.data[k] = v } func (c *Cache[K, V]) Get(k K) (V, bool) { v, ok := c.data[k] return v, ok } func main() { fmt.Println(Pair[string, int]{"port", 8080}) c := NewCache[string, float64](2) c.Put("EUR", 1.0) c.Put("USD", 1.17) c.Put("GBP", 0.87) // evicts EUR _, hasEUR := c.Get("EUR") gbp, _ := c.Get("GBP") fmt.Println(hasEUR, gbp) }
port=8080 false 0.87
Constraints that mention the type itself
Sometimes a constraint needs to say "a type with a method that takes and returns that same type". Since Go 1.26, a generic type's own name may appear in its type parameter list, which makes this direct:
package main import "fmt" type Adder[A Adder[A]] interface { Add(A) A } func SumAll[A Adder[A]](items []A) A { var total A for _, it := range items { total = total.Add(it) } return total } type Vec struct{ X, Y float64 } func (v Vec) Add(o Vec) Vec { return Vec{v.X + o.X, v.Y + o.Y} } type Money struct{ Cents int } func (m Money) Add(o Money) Money { return Money{m.Cents + o.Cents} } func main() { fmt.Println(SumAll([]Vec{{1, 2}, {3, 4}, {0.5, 0}})) fmt.Println(SumAll([]Money{{250}, {199}})) }
{4.5 6}
{449}Generic type aliases
A type alias can have type parameters too. That's mainly useful when moving types between packages or giving a long instantiation a shorter name:
package main import "fmt" type Pair[K comparable, V any] struct { Key K Value V } type Entry[V any] = Pair[string, V] // an alias with its own parameter func main() { e := Entry[int]{"retries", 3} var p Pair[string, int] = e // same type, no conversion needed fmt.Printf("%+v %T\n", p, e) }
{Key:retries Value:3} main.Pair[string,int]Limits to know
- You can't use a type switch or type assertion on a value of a type parameter's type directly; convert it to
anyfirst. - Interface types can't have generic methods (but since Go 1.27, other types' methods can — next lesson).
- A generic type must be instantiated before use:
Stack[int], notStack. When calling a generic function, the type arguments can usually be inferred.
Try it yourself
Write a generic Queue[T] with Enqueue, Dequeue and Len, and use it with two different element types.
serving 101 - waiting: 1 anyone waiting? false