Generics
Write one function for many types, and know when an interface is the better answer.
- Write a generic function with a type parameter and a constraint.
- Explain when an interface is a better choice than a type parameter.
- Predict which types a constraint like
~int | ~float64accepts.
What are generics for in Go?
Generics let you write one function or type that works for many types, without copying the code. Suppose you write a function that finds the largest number in a slice of int. Then you need one for float64. Then one for your own Cents type. The logic is identical each time, but Go's type checking will not let you pass a []float64 where a []int is expected, so you end up with three near-identical copies.
Generics let you write it once, with the type left as a blank to be filled in at the call site. The blank is called a type parameter, and a constraint says which types are allowed to fill it.
The next topic is the standard library: the packages that ship with Go. Much of what you might write generically is already there.
The syntax
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}names := Map(users, func(u User) string { return u.Name }) // T and U inferredConstraints limit what T can be
// "~int" means "any type whose underlying type is int", so your own// "type UserID int" qualifies too.type Number interface { ~int | ~int64 | ~float64}func Sum[T Number](xs []T) T { var total T // the zero value of whatever T is for _, x := range xs { total += x } return total}// cmp.Ordered comes from the stdlib and covers everything that supports < >func Max[T cmp.Ordered](a, b T) T { if a > b { return a } return b}When should you use generics instead of interfaces?
- Use generics for containers (a typed cache, set, or queue) and functions over any slice or map. The
slicesandmapspackages in the stdlib are built this way. - Use an interface when the thing you care about is what a type *does*.
Shapeis an interface, not a generic. - Be wary of a generic `Repository[T]` for your data layer. It looks clever, but the constraints grow a method per query, the error messages get hard to read, and one entity that needs a different query breaks the shared shape.
Try it yourself
package mainimport ( "cmp" "fmt" "slices")type Number interface{ ~int | ~int64 | ~float64 }type Cents int // ~int means this qualifies toofunc Sum[T Number](xs []T) T { var total T for _, x := range xs { total += x } return total}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}func main() { fmt.Println(Sum([]int{1, 2, 3})) fmt.Println(Sum([]float64{1.5, 2.5})) fmt.Println(Sum([]Cents{199, 250})) names := Map([]int{1, 2, 3}, func(n int) string { return fmt.Sprint("#", n) }) fmt.Println(names) xs := []int{3, 1, 2} slices.Sort(xs) fmt.Println(xs, slices.Contains(xs, 2), slices.Index(xs, 3)) fmt.Println(max(3, 9), cmp.Compare(1, 2))}- A type parameter is a blank filled in at the call site; a constraint says what may fill it.
~intmeans "any type whose underlying type is int", so your own named types qualify.- Generics are for types; interfaces are for behaviour. Do not swap them.
- Write it concretely twice before you generalise.
Filter, Map and Sum, then call Sum with int, float64 and your own named type.Max[T cmp.Ordered](s []T) (T, bool): return the largest element and true, or the zero value and false for an empty slice. It must work for ints, floats, strings, and your own types built on them (like type Celsius float64), which is what the ~ inside cmp.Ordered allows. Example: Max([3 -1 7 7]) returns 7, true.Tab indents. Press Esc, then Tab, to leave the editor.
- 1Drop the
~from a constraint and watch your named type stop compiling. - 2Write
Max[T cmp.Ordered]and use it on strings as well as numbers. - 3Try to write a generic
Repository[T]and see how quickly the constraints get ugly.