Generics
Type parameters arrived in Go 1.18 (March 2022) — late but deliberate. The team waited a decade to make sure the design didn't ruin the language. The result is narrow: generics for algorithms over containers, not for generic interfaces or methods. Most Go code still doesn't need them.
1 · The intuition
Before 1.18, writing Map, Filter, Reduce
in Go meant either using interface{} (lose type safety) or
writing the function once per type. Generics solve this — type parameters
carry through and the compiler enforces them.
But generics in Go are scoped tightly. You can have type parameters on functions and types. You cannot have them on methods, cannot define generic interfaces with type parameters in method signatures, cannot specialise on type. The design is intentional: keep it small.
2 · Try it — generic Map
package main
import "fmt"
// Map applies f to each element, returning a new slice.
// T and U are type parameters; "any" is a constraint that matches all types.
func Map[T, U any](xs []T, f func(T) U) []U {
result := make([]U, len(xs))
for i, x := range xs {
result[i] = f(x)
}
return result
}
func main() {
ints := []int{1, 2, 3, 4, 5}
doubled := Map(ints, func(n int) int { return n * 2 })
fmt.Println(doubled)
// Different output type
strs := Map(ints, func(n int) string { return fmt.Sprintf("#%d", n) })
fmt.Println(strs)
}Note: we didn't have to specify the type parameters at the call site.
The compiler infers them from the arguments. Use Map[int, string](...)
only if inference fails.
3 · Constraints — limiting what T can be
A constraint is an interface that the type parameter must satisfy. any
matches everything; comparable matches types usable with
== / !=; the cmp.Ordered constraint (Go
1.21) matches anything orderable.
package main
import (
"cmp"
"fmt"
)
// Sum requires types you can add. The constraint is a union.
type Number interface {
~int | ~int64 | ~float32 | ~float64
}
func Sum[T Number](xs []T) T {
var total T
for _, x := range xs {
total += x
}
return total
}
// Min requires types you can compare with <.
func Min[T cmp.Ordered](xs []T) T {
m := xs[0]
for _, x := range xs[1:] {
if x < m { m = x }
}
return m
}
func main() {
fmt.Println(Sum([]int{1, 2, 3})) // 6
fmt.Println(Sum([]float64{1.5, 2.5, 3.0})) // 7
fmt.Println(Min([]string{"go", "rust", "c"})) // c
fmt.Println(Min([]int{42, 7, 99})) // 7
}~int means "any type whose underlying
type is int" — so type Celsius int satisfies ~int.
Without the tilde, only int itself qualifies.4 · Generic types — a typed 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
}
last := s.items[len(s.items)-1]
s.items = s.items[:len(s.items)-1]
return last, true
}
func (s *Stack[T]) Len() int { return len(s.items) }
func main() {
s := &Stack[int]{}
s.Push(10)
s.Push(20)
s.Push(30)
fmt.Println("len:", s.Len())
for s.Len() > 0 {
v, _ := s.Pop()
fmt.Println(v)
}
}Note the limitation: you can't write func (s *Stack[T]) Map[U any](f func(T) U)
— methods can't have their own type parameters. Workaround: a top-level function
Map[T, U any](s *Stack[T], f func(T) U).
5 · slices & maps — the standard library answer
Go 1.21+ added slices and maps packages, which provide
the canonical generic utilities. Before you write your own, check these.
package main
import (
"fmt"
"slices"
"maps"
)
func main() {
xs := []int{3, 1, 4, 1, 5, 9, 2, 6}
slices.Sort(xs)
fmt.Println(xs) // [1 1 2 3 4 5 6 9]
fmt.Println(slices.Contains(xs, 5)) // true
fmt.Println(slices.Max(xs)) // 9
fmt.Println(slices.Min(xs)) // 1
m := map[string]int{"a": 1, "b": 2, "c": 3}
keys := slices.Collect(maps.Keys(m))
slices.Sort(keys)
fmt.Println(keys) // [a b c]
}6 · When generics, when interfaces?
| Situation | Reach for |
|---|---|
| "This function is identical except for the element type" | Generics — Map / Filter / Stack |
| "Different types should behave differently" | Interfaces — dispatch by method |
| "Need a typed container" | Generics — Set[T], OrderedMap[K, V] |
| "Plugin / extension point" | Interfaces — implementations decide behaviour |
| "Generic numeric algorithm" | Generics + constraint |
| "Method needs different return types per instance" | Methods can't be generic — restructure |
7 · From the wild
// Package cmp provides types and functions related to comparing ordered values.
// Ordered is a constraint that permits any ordered type:
// any type that supports the operators < <= >= >.
type Ordered interface {
~int | ~int8 | ~int16 | ~int32 | ~int64 |
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr |
~float32 | ~float64 |
~string
}
// Compare returns
// -1 if x is less than y,
// 0 if x equals y,
// +1 if x is greater than y.
func Compare[T Ordered](x, y T) int {
xNaN := isNaN(x)
yNaN := isNaN(y)
if xNaN && yNaN { return 0 }
if xNaN || x < y { return -1 }
if yNaN || x > y { return +1 }
return 0
}8 · Coming from another language?
| If you know… | The bridge |
|---|---|
| Java / C# | Like generics but simpler. No covariance/contravariance. No wildcards. No type erasure — Go monomorphises but cleverly (GC-shape stenciling). |
| TypeScript | Closer to TS generics than Java's. Constraints feel like extends. No conditional types. |
| Rust | Constraints are like traits but smaller. No associated types. No impl Trait. Compilation is faster because the design is simpler. |
| C++ | Like templates but checked at the call site, not at instantiation. No SFINAE. Errors are readable. |
9 · Common mistakes
- Reaching for generics first. Most Go problems don't need them. Try the concrete type or an interface first; generics only when duplication is real.
- Methods with type parameters. They don't exist. Move the type parameter to a top-level function.
- Forgetting
~in constraints.~intmatchestype Celsius int; plainintdoes not. - Generic functions that should be interfaces. If T's only use is "call methods on it", the function should accept an interface, not a type parameter.
- Over-constraining.
anyis fine when you don't need to do anything T-specific. Don't add a constraint until you need it.
10 · Exercises (~15 min)
- Implement Filter.
func Filter[T any](xs []T, pred func(T) bool) []T. Test on numbers (keep evens) and strings (length ≥ 3). - Reduce/Fold.
func Reduce[T, U any](xs []T, init U, f func(U, T) U) U. Use it to sum, multiply, and concat strings. - Set[T]. Build a generic set backed by
map[T]struct{}. T must becomparable. Methods: Add, Remove, Has, Len. - Sort by key. Use
slices.SortFuncto sort a slice of structs by a derived integer field. Read the docs once.
11 · When it clicks
- You reach for
slicesandmapspackages before writing your own helpers. - You can distinguish at a glance: "this wants generics" vs "this wants an interface".
- You stop typing
interface{}and reach for type parameters when types matter. - You know about the methods-can't-be-generic limitation and route around it.