Go path · Values and types · Topic 6 of 13

Generics and constraints

Not started

Go 1.18 added type parameters: functions and types can take types as parameters, written in square brackets. Each type parameter has a constraint, which is an interface. A constraint can list methods, as interfaces always could, and it can also list types, which describe the set of types allowed. Go 1.27 extended this to methods, which can now declare type parameters of their own.

Why it matters: Since Go 1.18 you can write one Map, Max or cache for many types without falling back to any and type assertions. Knowing what constraints mean keeps generic code simple.

  1. 1Type parameters in brackets

    func Max[T cmp.Ordered](a, b T) T declares T, constrained to types that support < and >. Callers usually leave T out and let Go infer it.

  2. 2Constraints are interfaces

    A constraint lists methods, types, or both. ~int means every type whose underlying type is int, including your own named types.

  3. 3Generic methods arrived in Go 1.27

    Before 1.27 only functions and types could have type parameters. Now a method can declare its own, but interface methods still can't.

  4. 4Reach for them for containers and algorithms

    If an ordinary interface does the job, it is usually clearer. Generics shine when the caller should get its own type back.

1

How it works

The explanation, with examples you can run.

Functions and constraints

package main import (	"cmp"	"fmt") type Number interface {	~int | ~int64 | ~float64} func Sum[T Number](xs []T) T {	var total T	for _, x := range xs {		total += x	}	return total} func MaxOf[T cmp.Ordered](xs ...T) T {	m := xs[0]	for _, x := range xs[1:] {		m = max(m, x)	}	return m} type Celsius float64 // underlying type float64, so it satisfies ~float64 func main() {	fmt.Println(Sum([]int{1, 2, 3}))	fmt.Println(Sum([]Celsius{20.5, 21}))	fmt.Println(MaxOf("pear", "apple", "zucchini"))}

It prints 6, 41.5 and zucchini. Notice:

  • Inference. Sum([]int{...}) doesn't say Sum[int]; the compiler works it out from the arguments.
  • ~T means "any type whose underlying type is T". Without the tilde, Celsius would be rejected.
  • The operations allowed on a T are those that every type in the constraint's set supports. That is why total += x compiles: all three types support +.
  • Built-in constraints. any allows any type. comparable allows ==, which map keys need. cmp.Ordered (Go 1.21) allows <. The built-in min and max also arrived in 1.21.

Generic types

type Set[T comparable] map[T]struct{} func (s Set[T]) Add(v T)           { s[v] = struct{}{} }func (s Set[T]) Has(v T) bool      { _, ok := s[v]; return ok }

A generic type must be instantiated before use (Set[string]{}). The standard library now has many generic helpers: slices.Sort, slices.Index, maps.Keys and the iterator types in iter (Go 1.23).

Generic methods (Go 1.27)

Until Go 1.26 a method could only use its receiver's type parameters, so helpers that needed a new one had to be package-level functions. Go 1.27 lets a method declare its own:

func (b *Bag) Count[T any]() int {	n := 0	for _, it := range b.items {		if _, ok := it.(T); ok {			n++		}	}	return n}

Called as b.Count[int](). Interface methods still can't have type parameters, and a generic method can't be used to satisfy an interface method. math/rand/v2 now has a (*Rand).N method alongside the generic rand.N function.

How it compiles

The language leaves the strategy to the compiler. The gc compiler doesn't usually make one copy per type argument: it compiles one copy per shape, where the shape depends on properties such as the type's size and where it holds pointers, so many type arguments share one compiled body. That shared body works much like a function with an interface parameter, so generic code is not automatically as fast as code written for one type. Measure before rewriting a hot path.

When to use them

Use generics for containers, and for algorithms over slices, maps and channels, where the caller should get its own element type back. When the code calls a few methods on a value, an ordinary interface parameter is usually simpler to read and just as fast.

2

Check yourself

3 questions. Get them all right to finish this topic.

  1. 1.type Celsius float64. Does Sum[Celsius] compile when the constraint is interface{ int | float64 }?
  2. 2.Which constraint lets a generic function use == on its values?
  3. 3.In Go 1.27, which of these is still not allowed?

0 of 3 right so far. Get all three to finish this topic.