Advanced Generics
Prerequisites: Race conditions & sync/atomic
Level 2 used the built-in constraints any, comparable, and
cmp.Ordered. This lesson covers writing your own constraints, generic
types, and — just as important — when to leave generics out.
Type sets and ~
A constraint is an interface. Besides methods, it can list the types it
permits, separated by |. A leading ~ means "any type whose underlying
type is this":
package main
import "fmt"
type Number interface {
~int | ~int64 | ~float64
}
func Sum[T Number](xs []T) T {
var total T
for _, x := range xs {
total += x
}
return total
}
type Celsius float64
func main() {
fmt.Println(Sum([]int{1, 2, 3}))
fmt.Println(Sum([]Celsius{20.5, 21.5}))
}
Expected output:
6
42
Without the ~, Celsius would be rejected: its underlying type is
float64, but it isn't float64. Interfaces that contain type elements like
this can only be used as constraints, not as ordinary variable types.
A constraint can combine a type set with methods:
type StringerInt interface {
~int
String() string
}
Generic types
Types can have type parameters too. A stack works for any element type:
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 s Stack[string]
s.Push("a")
s.Push("b")
v, ok := s.Pop()
fmt.Println(v, ok, s.Len())
var nums Stack[int]
_, ok = nums.Pop()
fmt.Println(ok)
}
Expected output:
b true 1
false
Note var zero T — the idiomatic way to get the zero value of an unknown
type. Methods use the receiver's type parameters (Stack[T]) but cannot
declare new type parameters of their own; write a top-level function
instead:
// Not allowed: func (s *Stack[T]) Map[U any](f func(T) U) *Stack[U]
func MapStack[T, U any](s *Stack[T], f func(T) U) *Stack[U] {
out := &Stack[U]{}
for _, v := range s.items {
out.Push(f(v))
}
return out
}
Multiple type parameters and inference
func Map[T, U any](xs []T, f func(T) U) []U {
out := make([]U, 0, len(xs))
for _, x := range xs {
out = append(out, f(x))
}
return out
}
// Type arguments are inferred from the call:
// lengths := Map([]string{"go", "rust"}, func(s string) int { return len(s) })
The compiler infers type arguments from the function arguments in most
cases. You only spell them out (Map[string, int](...)) when there's nothing
to infer from.
When NOT to use generics
Generics remove duplication in code that is identical apart from types — containers, and algorithms over slices, maps, and channels. They are the wrong tool when:
- Behaviour differs per type. If you'd need a type switch inside the generic function, use an interface with a method instead.
- An interface already expresses it.
func Save(w io.Writer)is simpler thanfunc Save[W io.Writer](w W)and just as flexible. - There's only one type today. Write the concrete version; generalise when a second real caller appears.
A useful rule of thumb: write the concrete code first, and reach for type parameters when you find yourself writing the same function a third time.
Common mistakes
- Forgetting
~and then wondering why a named type liketype UserID intdoesn't satisfy the constraint. - Using
anythen trying to use<,+, or==— the constraint must permit the operator (cmp.Ordered, a custom type set, orcomparable). - Returning
nilfor aT—nilisn't valid for every type. Returnvar zero T. - Making every helper generic "for flexibility". It makes signatures harder to read with no benefit.
Practice
Write a generic Set[T comparable] with methods Add(v T), Has(v T) bool,
and Len() int, plus a constructor NewSet. Add "go" twice and "rust"
once, then print Len() and Has("go").
Show solution
package main
import "fmt"
type Set[T comparable] struct {
m map[T]struct{}
}
func NewSet[T comparable]() *Set[T] {
return &Set[T]{m: make(map[T]struct{})}
}
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 main() {
s := NewSet[string]()
s.Add("go")
s.Add("go")
s.Add("rust")
fmt.Println(s.Len(), s.Has("go"))
}
Expected output:
2 true
NewSet[string]() needs an explicit type argument because there is no
function argument to infer T from.