50. Constraints

📖 Reading · 11 min
💡 Most code boxes below are live — edit one and hit Run. Boxes without a Run button are reference-only (they can't run in your browser).

A constraint answers one question: what is this generic code allowed to do with T? It's written as an interface — but a constraint interface can list types as well as methods, which is the feature that makes generics work.

Named constraints

Inline unions get long. Give them a name with type:

package main

import "fmt"

type Number interface {
    int | int8 | int16 | int32 | int64 | float32 | float64
}

func Sum[T Number](nums []T) T {
    var total T
    for _, n := range nums {
        total += n
    }
    return total
}

func Average[T Number](nums []T) float64 {
    if len(nums) == 0 {
        return 0
    }
    return float64(Sum(nums)) / float64(len(nums))
}

func main() {
    ints := []int{10, 20, 30}
    floats := []float64{1.5, 2.5, 3.0}

    fmt.Println(Sum(ints), Sum(floats))
    fmt.Printf("%.2f %.2f\n", Average(ints), Average(floats))
}

Number is an interface used only as a constraint — you can't declare a variable of type Number, because it has no methods to call. Constraint interfaces and ordinary interfaces are written the same way and used differently.

~ — including named types

There's a gap in that constraint. type Celsius float64 is not float64, so it wouldn't satisfy Number. The tilde fixes it:

package main

import "fmt"

type Number interface {
    ~int | ~int64 | ~float64
}

type Celsius float64
type Miles float64
type UserID int

func Sum[T Number](nums []T) T {
    var total T
    for _, n := range nums {
        total += n
    }
    return total
}

func main() {
    fmt.Println(Sum([]int{1, 2, 3}))
    fmt.Println(Sum([]Celsius{20.5, 1.5}))
    fmt.Println(Sum([]Miles{10, 5}))
    fmt.Println(Sum([]UserID{1, 2}))
}

~float64 means "float64, or any type whose underlying type is float64". Since defining named types over primitives is idiomatic Go (module 5), constraints in real code almost always use ~.

Rule of thumb: write ~T, not T, in constraints unless you deliberately want to exclude named types.

The standard constraints

Go builds two in:

  • any — no constraint at all. Store it, copy it, pass it. Nothing else.
  • comparable — supports == and !=. Required for map keys and for anything doing equality checks.
package main

import "fmt"

func Unique[T comparable](items []T) []T {
    seen := make(map[T]struct{}, len(items))
    var out []T
    for _, v := range items {
        if _, ok := seen[v]; ok {
            continue
        }
        seen[v] = struct{}{}
        out = append(out, v)
    }
    return out
}

func main() {
    fmt.Println(Unique([]int{1, 2, 2, 3, 1}))
    fmt.Println(Unique([]string{"go", "rust", "go"}))
}

map[T]struct{} only compiles because T is comparable. Try changing the constraint to any and the compiler will tell you exactly that.

There's also cmp.Ordered in the standard library (Go 1.21+), covering every type that supports <:

import "cmp"

func Min[T cmp.Ordered](a, b T) T {
    if a < b {
        return a
    }
    return b
}

It's defined as ~int | ~int8 | ... | ~float64 | ~string — exactly the hand-written constraint you'd otherwise repeat in every project. (The in-browser interpreter here predates the cmp package, so the runnable boxes on this page define their own Ordered.)

Method constraints

A constraint can require methods, just like a normal interface:

package main

import "fmt"

type Stringer interface {
    String() string
}

func JoinAll[T Stringer](items []T, sep string) string {
    out := ""
    for i, it := range items {
        if i > 0 {
            out += sep
        }
        out += it.String()
    }
    return out
}

type Money struct{ Cents int }

func (m Money) String() string {
    return fmt.Sprintf("$%d.%02d", m.Cents/100, m.Cents%100)
}

type Tag string

func (t Tag) String() string { return "#" + string(t) }

func main() {
    fmt.Println(JoinAll([]Money{{199}, {2500}}, ", "))
    fmt.Println(JoinAll([]Tag{"go", "generics"}, " "))
}

Now the body can call it.String(), because every allowed T has one.

Worth pausing on: this could have been func JoinAll(items []Stringer, sep string) with an ordinary interface. What did generics buy? Type safety at the boundaryJoinAll([]Money{...}) guarantees every element is a Money, and no boxing happens. With []Stringer you could mix Money and Tag in one slice, which is sometimes what you want and sometimes a bug.

Combining methods and types

package main

import "fmt"

type Ordered interface {
    ~int | ~int64 | ~float64 | ~string
}

func MaxOf[T Ordered](items []T) (T, bool) {
    var best T
    if len(items) == 0 {
        return best, false
    }
    best = items[0]
    for _, v := range items[1:] {
        if v > best {
            best = v
        }
    }
    return best, true
}

func main() {
    fmt.Println(MaxOf([]int{3, 9, 4}))
    fmt.Println(MaxOf([]string{"go", "zig", "c"}))
    fmt.Println(MaxOf([]float64{}))
}

Returning (T, bool) instead of panicking on an empty slice is the same comma-ok convention you've seen with maps, channels and type assertions — generic code follows the same idioms as everything else.

Constraints as documentation

The constraint is the clearest statement of what your function needs:

constraint means
any I only store, copy and pass it
comparable I use == (map keys, dedup, lookup)
~int \| ~float64 I do arithmetic
Ordered I compare with <
Stringer I call String()
interface{ ~[]byte \| ~string } I index or take len

Pick the weakest one that lets your body compile. A function constrained to comparable works for far more callers than one constrained to int, and the compiler still verifies everything.

Your turn

Define an Ordered constraint and write Min that returns the smallest element of a slice, plus false if the slice is empty:

2 true
apple true
0 false
package main

import "fmt"

// define Ordered, then Min[T Ordered](items []T) (T, bool)

func main() {
    fmt.Println(Min([]int{5, 2, 9}))
    fmt.Println(Min([]string{"pear", "apple"}))
    fmt.Println(Min([]int{}))
}
package main

import "fmt"

type Ordered interface {
    ~int | ~int64 | ~float64 | ~string
}

func Min[T Ordered](items []T) (T, bool) {
    var best T
    if len(items) == 0 {
        return best, false
    }
    best = items[0]
    for _, v := range items[1:] {
        if v < best {
            best = v
        }
    }
    return best, true
}

func main() {
    fmt.Println(Min([]int{5, 2, 9}))
    fmt.Println(Min([]string{"pear", "apple"}))
    fmt.Println(Min([]int{}))
}

Next: generic types, not just generic functions.