31.3 Constraints: Limiting What T Can Be
Right, so you’ve got your shiny new type parameter T and you’re ready to write some beautifully generic code. You try to write a function that sums a slice of numbers, func Sum[T any](s []T) T, and immediately you hit a wall. You can’t use the + operator. Why? Because any means literally any type. You could be summing []int, []string, or []http.Request—and Go’s compiler, being the stubbornly pragmatic friend that it is, refuses to let you add two http.Requests together. This is the problem constraints are designed to solve.
A constraint is simply an interface. That’s the big secret. It’s not a new language concept; it’s an interface used in a new, powerful way. It defines the set of permissible type arguments. When we say T any, we’re using the predeclared any interface (which is literally interface{}). A more useful constraint is an interface that actually has methods.
The Classic: Using Interface Methods as Constraints
Let’s say you want a function that returns the string representation of a value, but you want it to work for any type that already knows how to string itself. This is a job for the good old String() string method, aka the Stringer interface.
// Stringify returns the string representation of a value.
// It works for any type T that implements the String() method.
func Stringify[T fmt.Stringer](val T) string {
return val.String()
}
type MyThing struct {
Name string
}
func (m MyThing) String() string {
return "MyThing: " + m.Name
}
func main() {
thing := MyThing{Name: "Gopher"}
fmt.Println(Stringify(thing)) // Output: MyThing: Gopher
// This would fail to compile because int doesn't have a String() method.
// Stringify(42)
}
The constraint [T fmt.Stringer] is brilliant because it’s self-documenting. It screams, “Give me anything that can String() itself!” The compiler checks that the type argument (MyThing) satisfies the interface (fmt.Stringer), and we’re off to the races. This is the most straightforward and common use of constraints.
The New Hotness: Interface Type Lists
But what about our Sum function? Integers, floats, and complex numbers don’t share a common method interface; they share operators. This is where Go 1.18 introduced a new trick: interfaces can now include type sets, often called “type lists.”
The standard library wisely packaged up the most common ones for you in the golang.org/x/exp/constraints package (note: it’s experimental, exp for “experimental”) and then cemented the most crucial ones into the actual builtin package in Go 1.21 with the new comparable and any identifiers.
Let’s build our Sum function properly.
import "golang.org/x/exp/constraints"
// Sum adds all the elements in a slice. It works for any type that supports the + operator.
func Sum[T constraints.Ordered](s []T) T {
var sum T
for _, v := range s {
sum += v // The + operator is now valid because T is constrained!
}
return sum
}
func main() {
ints := []int{1, 2, 3, 4}
fmt.Println(Sum(ints)) // Output: 10
floats := []float64{1.1, 2.2, 3.3}
fmt.Println(Sum(floats)) // Output: 6.6
}
The magic is in constraints.Ordered. If you look at its definition, it’s an interface that includes all types that can be ordered with <, >, <=, and >= (which also implies they support +, -, *, etc.). It’s a union of types: ~int | ~int8 | ~int16 | ... | ~float64 | ~string.
Ah, the tilde (~). This is a crucial bit of nuance. ~int means “the type int *or any type whose underlying type is int.” This is what lets your type aliases and custom types work seamlessly with generics.
type MySpecialInt int
func main() {
myInts := []MySpecialInt{1, 2, 3}
// Without the ~ in the constraint definition, this wouldn't work.
// MySpecialInt's underlying type is int, so ~int includes it.
fmt.Println(Sum(myInts)) // Output: 6
}
The Built-in comparable Constraint
The other built-in constraint you’ll use constantly is comparable. It’s exactly what it sounds like: it allows any type that can be compared with == and !=. This is indispensable for writing functions that need to check for equality, like finding an item in a slice or implementing a set.
// Contains returns true if the target value is present in the slice.
func Contains[T comparable](s []T, target T) bool {
for _, v := range s {
if v == target { // This is only valid because T is comparable
return true
}
}
return false
}
func main() {
if Contains([]string{"go", "rust", "zig"}, "zig") {
fmt.Println("Found Zig!") // This will print
}
}
A critical pitfall here: while comparable includes structs and arrays, it excludes functions, slices, and maps. This is a common compile-time error. You try to pass a []string to a T comparable and the compiler will rightly tell you that slices aren’t comparable. It’s annoying, but it’s saving you from a runtime panic.
Rolling Your Own Constraints
You aren’t limited to the standard library’s constraints. You can build your own powerful ones by combining interfaces and type sets. This is where you can really enforce your API’s requirements.
// Numeric expresses a type that is either a complex number, an integer, or a float.
// This is a union of several type sets.
type Numeric interface {
~complex64 | ~complex128 | constraints.Integer | constraints.Float
}
// AddTwo adds two values of any numeric type.
func AddTwo[T Numeric](a, b T) T {
return a + b
}
func main() {
fmt.Println(AddTwo(3, 5)) // int: 8
fmt.Println(AddTwo(1.5, 2.7)) // float64: 4.2
fmt.Println(AddTwo(1+2i, 3+4i)) // complex128: (4+6i)
}
The best practice is to declare your custom constraints at the package level. This makes them reusable and turns what would be a messy, inline type union into a clean, self-documenting contract. It tells the reader exactly what kind of type your generic function is expecting, and it gives the compiler a single, clear rule to enforce. That’s the whole point: giving the compiler just enough information to get out of your way and let you write powerful, type-safe code.