Constraints

A constraint lists the types a parameter accepts and decides what the body may do: any, comparable, or an interface with a type set.

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

Every type parameter has a constraint. It is written where the type of an ordinary parameter would go, and it does two jobs. It says which types a caller may supply, and it decides what the body is allowed to do with values of that type.

The two predeclared names

any allows every type. A body with any can store values and pass them on, nothing more. comparable allows the types that work with == and !=, which is exactly what a map key needs. Slices, maps and functions are not comparable, so they are not in that set.

Constraint interfaces

A constraint is an interface. As well as methods, an interface may list types, separated by |. That list is called a type set.

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

A leading ~ means "this type, or any type whose underlying type is this". Without it, a defined type such as type Celsius float64 is rejected, because Celsius is its own type. With ~float64 it is accepted. The operators the body may use are the ones every type in the set supports. All three above support +, so += compiles.

The standard library ships one ready made. cmp.Ordered, added in Go 1.21, is the set of every integer, floating-point and string type in tilde form, so < and > work.

What trips people up

An interface that lists types, or that embeds comparable, may only be used as a constraint. var n Number does not compile. An interface with only methods, such as fmt.Stringer, works in both places.

Example

Example · go
package main

import (
	"cmp"
	"fmt"
)

// Number lists the types Sum accepts. The ~ means "or any type whose
// underlying type is this one".
type Number interface {
	~int | ~int64 | ~float32 | ~float64
}

// Celsius has float64 underneath, so ~float64 accepts it.
type Celsius float64

func Sum[T Number](xs []T) T {
	var total T // zero value: 0 for every type in the set
	for _, x := range xs {
		total += x
	}
	return total
}

// Largest is also called with strings, which Number does not accept, so
// it uses the standard library constraint cmp.Ordered (Go 1.21).
func Largest[T cmp.Ordered](xs []T) T {
	best := xs[0]
	for _, x := range xs[1:] {
		if x > best {
			best = x
		}
	}
	return best
}

func main() {
	fmt.Println(Sum([]int{1, 2, 3}))
	fmt.Println(Sum([]Celsius{19.5, 0.5}))
	fmt.Println(Largest([]string{"pear", "apple", "fig"}))

}

// Output:
// 6
// 20
// pear

When to use it

  • Let a defined type such as type UserID int into a numeric helper by writing ~int.
  • Require == so a helper can build a set or use the value as a map key.
  • Use cmp.Ordered for a Max helper that serves ints, floats and strings.
  • Demand both equality and a String method from a single type parameter.

More examples

The tilde, and what it changes

The difference between Strict and Loose is one character and it decides whether a whole class of callers can use the function. Codebases that give meaning to numbers with defined types, such as UserID, Cents or Celsius, need the tilde form everywhere, otherwise every call site has to convert back to the plain type and the type safety is lost.

Example · go
package main

import "fmt"

type Celsius float64

// Strict accepts float64 and nothing else.
type Strict interface{ float64 }

// Loose accepts any type whose underlying type is float64.
type Loose interface{ ~float64 }

func Twice[T Loose](v T) T { return v * 2 }

func main() {
	// T is float64 here.
	fmt.Println(Twice(1.5))
	// T is Celsius here. With the Strict constraint this call would be
	// rejected: Celsius is its own type, not float64.
	fmt.Println(Twice(Celsius(20)))

}

// Output:
// 3
// 40

comparable, because == is needed

comparable is the constraint to pick the moment a body compares two values or uses one as a map key. Note what it excludes: add a []string field to Code and IndexOf([]Code{...}) stops compiling, because a struct containing a slice is not comparable. The standard library makes the same choice in slices.Index, declared as Index[S ~[]E, E comparable](s S, v E) int.

Example · go
package main

import "fmt"

type Code struct {
	Sys  string
	Name string
}

// IndexOf reports where v sits in s, or -1. The body uses ==, so any
// would not compile here; comparable is the smallest constraint that
// allows it.
func IndexOf[T comparable](s []T, v T) int {
	for i, got := range s {
		if got == v {
			return i
		}
	}
	return -1
}

func main() {
	fmt.Println(IndexOf([]string{"a", "b"}, "b"))
	c := Code{Sys: "icd", Name: "J45"}
	// A struct whose fields are all comparable is comparable itself.
	fmt.Println(IndexOf([]Code{c}, c))

}

// Output:
// 1
// 0

Two requirements in one constraint

Constraints combine by embedding, and the combined interface can mix a type set, comparable and method requirements. Dedup needs the map key (comparable) and the printed form (String), and asking for both in the constraint means neither is checked at run time. Because Keyed embeds comparable it can only be used as a constraint, never as the type of a variable.

Example · go
package main

import "fmt"

type Code struct {
	Sys  string
	Name string
}

func (c Code) String() string { return c.Sys + ":" + c.Name }

// Keyed demands both: == (from comparable) and a String method (from
// fmt.Stringer). Embedding both in one interface is how you say "and".
type Keyed interface {
	comparable
	fmt.Stringer
}

// Dedup drops repeats and keeps the first occurrence of each value.
func Dedup[T Keyed](vals []T) []string {
	seen := make(map[T]bool, len(vals))
	out := make([]string, 0, len(vals))
	for _, v := range vals {
		if seen[v] {
			continue
		}
		seen[v] = true
		out = append(out, v.String())
	}
	return out
}

func main() {
	a := Code{Sys: "icd", Name: "J45"}
	b := Code{Sys: "icd", Name: "E11"}
	fmt.Println(Dedup([]Code{a, b, a}))

}

// Output:
// [icd:J45 icd:E11]

Discussion

  • Be the first to comment on this lesson.