50. Constraints
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 boundary — JoinAll([]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.