CHAPTER 22 · TYPES & ABSTRACTIONS

Generics

Generics (Go 1.18+) let you write functions and types parameterized by the types they work with. Before generics, stuff like "generic min" or a reusable "stack" required interface{} + runtime type assertions. Now the compiler can check everything statically.

Learning objectives

  • Declare generic functions with type parameters.
  • Constrain type parameters via interfaces (including unions).
  • Define generic structs and methods.
  • Use the comparable built-in constraint.
  • Know when generics aren't worth the complexity.

Type parameters

func PrintSlice[T any](s []T) {
    for _, v := range s {
        fmt.Println(v)
    }
}

PrintSlice([]int{1, 2, 3})
PrintSlice([]string{"a", "b"})

[T any] declares a type parameter T with constraint any (accepts every type). Inside the body, T behaves like a regular type.

Constraints

A constraint is any interface. Methods required by the interface are available on the type parameter:

type Stringer interface { String() string }

func JoinStrings[T Stringer](xs []T, sep string) string {
    parts := make([]string, len(xs))
    for i, x := range xs {
        parts[i] = x.String()   // allowed because T: Stringer
    }
    return strings.Join(parts, sep)
}

Union constraints

You can express "one of these types" with |:

type Number interface {
    ~int | ~int64 | ~float64        // | = union; ~ = "any type whose underlying type is"
}

func Sum[T Number](xs []T) T {
    var s T
    for _, x := range xs { s += x }
    return s
}

fmt.Println(Sum([]int{1, 2, 3}))          // 6
fmt.Println(Sum([]float64{1.5, 2.5}))     // 4.0

The ~ ("tilde") lets custom types like type Cents int also satisfy the constraint. Otherwise only the exact listed types would.

The comparable built-in

comparable is a predeclared constraint for any type that supports == and !=:

func Contains[T comparable](xs []T, target T) bool {
    for _, x := range xs {
        if x == target { return true }
    }
    return false
}

Generic types

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
}

s := &Stack[int]{}
s.Push(1); s.Push(2)
v, _ := s.Pop()
fmt.Println(v)     // 2

Generic methods

Methods on a generic type automatically see its type parameters. You can't add extra type parameters on a method that the type doesn't already declare. This is a deliberate design choice.

Type inference

Most of the time you don't need to specify the type argument; the compiler infers it from the arguments:

PrintSlice([]int{1, 2, 3})       // T inferred as int
PrintSlice[int]([]int{1, 2, 3})  // explicit: rarely needed

When to use generics (and when not to)

Good cases:

  • Container types: Set[T], Queue[T], LinkedList[T].
  • Utility functions over slices/maps: Map, Filter, Reduce.
  • Libraries that need to work across multiple related types.

Bad cases:

  • Code that handles a single type. Just use that type.
  • Interface-based polymorphism that already works well. Don't rewrite io.Reader usage to be generic.
  • Clever constraints that sacrifice readability. Generics can make code harder to read.
Rule of thumb Start without generics. Refactor to generics only when you've got clear code duplication across types and no interface can capture the shared behavior. The Go stdlib's slices and maps packages are good examples of generics well-used.

Check your understanding

Practice exercises

EXERCISE 1

Generic Map function

Write Map[T, U any](xs []T, f func(T) U) []U that applies f to each element of xs and returns a new slice.

Show solution
func Map[T, U any](xs []T, f func(T) U) []U {
    out := make([]U, len(xs))
    for i, x := range xs {
        out[i] = f(x)
    }
    return out
}

nums := []int{1, 2, 3}
strs := Map(nums, func(n int) string { return fmt.Sprintf("n=%d", n) })
fmt.Println(strs)    // [n=1 n=2 n=3]

Further reading

Type-safe polymorphism achieved.