Dev Skills

Generics

Write one function for many types, and know when an interface is the better answer.

Topic 44 of 98 · 5 min to read · 45 min to build
After this topic you can
  • 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 | ~float64 accepts.

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

generics.go
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 inferred

Constraints limit what T can be

constraints.go
// "~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 slices and maps packages in the stdlib are built this way.
  • Use an interface when the thing you care about is what a type *does*. Shape is 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

press Run — this executes on the Go Playground
package main
import (
"cmp"
"fmt"
"slices"
)
type Number interface{ ~int | ~int64 | ~float64 }
type Cents int // ~int means this qualifies too
func 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))
}
One function, several types, and the stdlib helpers that already exist (`max` is a builtin since Go 1.21).
The short version
  • A type parameter is a blank filled in at the call site; a constraint says what may fill it.
  • ~int means "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.
Remember: If the duplication is between behaviours, you wanted an interface, not a generic.
Build this before moving on
Write generic Filter, Map and Sum, then call Sum with int, float64 and your own named type.
Coding challenge · checked by tests
Write the generic function 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.

Then try these in your own editor
  1. 1Drop the ~ from a constraint and watch your named type stop compiling.
  2. 2Write Max[T cmp.Ordered] and use it on strings as well as numbers.
  3. 3Try to write a generic Repository[T] and see how quickly the constraints get ugly.
Check yourselfTake the Generics and stdlib test