31. Advanced generics: constraints, generic algorithms, inference limits
31.1 Custom constraint interfaces
type Numeric interface {
~int | ~int32 | ~int64 | ~float32 | ~float64
}
func Average[T Numeric](nums []T) float64 {
var sum T
for _, n := range nums {
sum += n
}
return float64(sum) / float64(len(nums))
}
The ~ ("approximation element") means "this type, or any type whose
underlying type is this" — so a type Celsius float64 still satisfies
~float64, letting your generic function work with named types, not
just the exact built-ins.
31.2 Generic functions over slices: Map, Filter, Reduce
func Map[T, U any](s []T, f func(T) U) []U {
out := make([]U, len(s))
for i, v := range s {
out[i] = f(v)
}
return out
}
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
}
names := Map(users, func(u User) string { return u.Name })
active := Filter(users, func(u User) bool { return u.Active })
The standard library's slices and maps packages (Go 1.21+) already
provide slices.Sort, slices.Contains, maps.Keys, and similar — reach
for those before writing your own.
31.3 Where type inference runs out
Generic methods (as opposed to generic functions/types) don't exist — you cannot add a new type parameter to a method, only use the type parameters already declared on the receiver type. And inference across multiple unrelated type parameters sometimes needs an explicit hint:
// Won't infer U from context alone in many expressions — spell it out:
result := Map[User, string](users, func(u User) string { return u.Name })
31.4 Real-world example
Rewriting the Storage interface from Chapter 9 as
Storage[K comparable, V any] gives you compile-time-checked,
zero-reflection generic storage backends — the exact upgrade path from
the 90-day plan's map[string]string-based cache to the
projects/lru_cache.md design in Chapter 26.