31.8 Constraints Defined in golang.org/x/exp/constraints
Alright, let’s talk about the golang.org/x/exp/constraints package. This is where the Go team put all the shiny, useful constraint toys for us to play with when generics landed. Think of it as the official, but slightly experimental, toolbox for describing what kinds of types your generic functions can accept.
First, a crucial reality check: this package lives in exp, which is Go-speak for “experimental.” This means the Go team reserves the right to change their minds, break your code, and move things into the main standard library whenever they feel like it. It’s incredibly useful, but you don’t want to bet your company’s core infrastructure on it without a clear exit strategy. Most of these constraints are so fundamental that they’ll likely be stabilized somewhere, but just be aware you’re living on the edge.
Now, why does this package exist? Because writing out interface constraints for every basic operation like “greater than” or “addition” would be a massive pain. You’d end up writing the same boilerplate ~int | ~int32 | ~int64... for every function that needs a number. The constraints package does that tedious work for you.
The Usual Suspects: Ordered and Numeric
The two biggest stars in this package are constraints.Ordered and constraints.Integer (or its cousin, constraints.Numeric).
Ordered is your go-to for anything that can be compared with <, <=, >, and >=. This is the secret sauce for writing a generic Max or Min function without losing your mind.
package main
import (
"fmt"
"golang.org/x/exp/constraints"
)
// Max returns the larger of two values.
func Max[T constraints.Ordered](a, b T) T {
if a > b {
return a
}
return b
}
func main() {
fmt.Println(Max(42, 99)) // 99 (int)
fmt.Println(Max(3.14, 2.71)) // 3.14 (float64)
fmt.Println(Max("z", "a")) // "z" (string)
// Max([]int{1}, []int{2}) // Compile-time error: []int does not satisfy Ordered (thank goodness)
}
See? Clean, simple, and it works for all the basic types you’d expect. The Ordered constraint includes all the signed and unsigned integers, floats, and string types. It’s a thing of beauty.
Now, what if you need to do arithmetic? That’s where Integer, Float, Complex, and the umbrella constraint Numeric come in.
// Sum adds up all the numbers in a slice.
func Sum[T constraints.Numeric](s []T) T {
var total T
for _, v := range s {
total += v // The += operator is the key here. It works because of Numeric.
}
return total
}
func main() {
ints := []int{1, 2, 3}
floats := []float64{1.1, 2.2, 3.3}
fmt.Println(Sum(ints)) // 6
fmt.Println(Sum(floats)) // 6.6
}
Numeric is a union of Integer, Float, and Complex. Use it when you truly don’t care what kind of number you’re dealing with, just that you can add, subtract, etc.
The Tilde (~) is Your Friend
You might have noticed I used ~int earlier. This is a critical detail. The tilde (~) in a constraint means “the underlying type of this type.” So ~int allows not just int, but any type defined with an underlying type of int.
This is why the constraints package uses them everywhere. It future-proofs your code against user-defined types.
type MySpecialInt int
func main() {
a, b := MySpecialInt(42), MySpecialInt(99)
fmt.Println(Max(a, b)) // This works because Ordered uses ~int, ~string, etc.
}
Without the tilde, MySpecialInt wouldn’t satisfy constraints.Ordered because it’s not literally int. The tilde is what makes these constraints genuinely useful in the real world.
The Edge Cases and Pitfalls
- No
Unsignedconstraint. Want a function that only takes unsigned integers? You have to roll your own union constraint:type Unsigned interface { ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr }. Annoying, but straightforward. constraints.Signedexists. It’s the counterpart to the missingUnsigned, covering~int,~int8, etc.- The
anyEquivalence.constraints.Complexis currently defined as~complex64 | ~complex128. That’s it. It’s functionally almost identical to just usinganyif you only ever pass it complex numbers, but it provides vital documentation and type safety at compile time. Don’t be lazy; use the right constraint. - The Experimental Sword of Damocles. I can’t stress this enough. If you use this package,
go.modwill pin an experimental version. Have a plan for what happens if the constraints get moved to the mainconstraintspackage ingo/constraintsor change entirely. Using a tool likegoforwardcan help you prepare for this eventual migration.
The golang.org/x/exp/constraints package is a masterclass in providing building blocks. It doesn’t try to do everything; it gives you the precise, composable pieces you need to build your own elegant generic code. Use it, but use it wisely.