Type Constraints in Depth
You already know that [T any] accepts any type and [T comparable] accepts
anything usable with ==. Both are constraints: interfaces used not as
values, but as bounds on a type parameter. This lesson is about writing your
own.
An interface can list types, not just methods
Before generics, an interface only listed methods. Since Go 1.18 an interface
can also contain a type set — a union of concrete types joined with |:
type Number interface {
~int | ~int64 | ~float64
}
Read Number as "the set of types that are int, int64, or float64". Used as a
constraint, [T Number] means "T may be any type in that set". This kind of
interface is constraint-only: you can bound a type parameter with it, but you
cannot declare a variable of type Number the way you can with a method
interface.
What ~ means
The tilde is the important part. ~int means "any type whose underlying
type is int" — not just int itself. So a defined type still fits:
type Celsius float64
func Abs[T Number](x T) T {
if x < 0 {
return -x
}
return x
}
var t Celsius = -4
_ = Abs(t) // Celsius satisfies ~float64, so this compiles
Without the tilde, float64 | int would match only those exact types, and
Celsius would be rejected. Because you almost always want your own defined
types to qualify, type sets are nearly always written with ~.
The set decides what the body may do
Here's the rule that makes constraints worth understanding: inside the function, you may use only the operations valid for every type in the set.
func Sum[T Number](xs []T) T {
var total T
for _, x := range xs {
total += x // OK: + works for int, int64, and float64
}
return total
}
+ and < are legal here because they work for all three members. Try to use
% and it won't compile — float64 has no remainder operator, so it isn't in
"every type in the set". That is also why any lets you do almost nothing with
a value except pass it around: its set is all types, and no operator is
common to all of them. The constraint isn't decoration; it is the exact
contract the compiler checks the body against.
Zero values still work
Every type has a zero value, so var zero T is always valid regardless of the
constraint. For a numeric T that zero is 0, which is exactly what an empty
Sum should return — no special case needed.
Why Go does it this way
Type sets keep generics small and fully checked. There's no separate template
language and no "instantiate and hope it compiles" — the constraint is an
ordinary interface, and both sides are verified up front: the body may only use
what the set guarantees, and callers may only pass types the set contains. One
idea, interface, now covers both behavior (method lists) and type-level
constraints. In the exercise you'll define a Number constraint and write a
sum over it that works for int, float64, and your own defined types
alike.
Practice
1. Sum over a Numeric Constraint
IntermediateImplement a generic
sumover a custom numeric constraint.The starter already defines the constraint — an interface whose type set is a union of underlying types:
type Number interface { ~int | ~int64 | ~float64 } func sum[T Number](xs []T) Tsumreturns the total of all elements inxs.Requirements:
- Add the elements with
+(legal because every type inNumbersupports it) and return the total. - An empty slice returns the zero value of
T— that is0for any numeric type, sovar total Tstarts you at the right place. - Don't change the signature or the constraint.
The tests call
sumwith[]int,[]float64, an empty slice, and a slice of a defined type (type Score int). The~intin the constraint is what letsScore— a distinct type whose underlying type isint— qualify.package main import "fmt" // Number is the set of types this exercise works over: anything whose // underlying type is int, int64, or float64. type Number interface { ~int | ~int64 | ~float64 } // sum returns the total of xs. For an empty slice it returns the zero value // of T (which is 0 for any numeric type). func sum[T Number](xs []T) T { var zero T return zero // TODO: implement } func main() { fmt.Println(sum([]int{1, 2, 3})) fmt.Println(sum([]float64{1.5, 2.5})) }Sign in to write and run your solution in the browser.
Sign in for hints and the solution.
- Add the elements with