The golang.org/x/exp/constraints package ships a small set of named type constraints (Ordered, Integer, Float, Signed, Unsigned, Complex) that almost every generic Go function ends up needing. Instead of hand-rolling a type Number interface { ~int | ~int8 | ... } in every file, you import the package and use what's already there. This chapter covers what each constraint actually contains, how to compose them, why the package still lives under x/exp, and how Go 1.21's stdlib cmp.Ordered together with the slices and maps packages changes the picture.
When you write a generic function that does math or comparison, you keep needing the same handful of constraints over and over. Sort a slice? You need "anything that supports <." Sum a slice? You need "any numeric type." Bucket histogram counts? You need "any integer type."
Without a shared library, every file ends up with its own copy of the same type set:
These definitions show up in repo after repo, often with slight inconsistencies. One file forgets uintptr. Another writes int without the ~ and breaks for named types like type Cents int. A third spells the constraint Numeric instead of Number, so two halves of the codebase can't share a generic function.
The constraints package solves this by being the canonical definition. When you import it, every other Go developer who reads your code immediately knows what constraints.Ordered means, what types it admits, and how it relates to the other constraints in the package.
Read the diagram as containment. Ordered is the widest of the comparison constraints: it admits every type that supports the <, <=, >, >= operators. That means all integers, all floats, and string. Integer is narrower; it admits only integer types, split further into Signed and Unsigned. Complex sits off to the side because complex numbers aren't ordered (Go doesn't define < on them), so they can't appear under Ordered.
Cents, Quantity, Price, and every other named numeric type you'll define in an e-commerce program satisfies the relevant constraint automatically, thanks to the ~ tilde approximation that we'll look at next.
The package lives outside the standard library, in the golang.org/x/exp experimental repository. You bring it in like any other module dependency:
This adds the requirement to your go.mod file. After that, import it like any package:
A minimal go.mod after running the go get looks roughly like this:
The exact version string is a pseudo-version because the x/exp repo doesn't tag releases. Your version will differ; let go get and go mod tidy manage it.
Cost: x/exp is explicitly an experimental tree. Its packages may move, change, or be promoted into the standard library. We'll discuss the implications later in this chapter when we look at why cmp.Ordered exists.
Here's the smallest useful program that uses the package. It computes the minimum of three prices typed as Cents (a named integer type):
Cents is a named type whose underlying type is int64. It satisfies constraints.Ordered because the constraint uses the ~ tilde, which we'll dissect in the next section.
constraints.OrderedOrdered is the most-used constraint. It captures every Go type that supports the four ordering operators. The package's source definition is equivalent to this:
Two details matter. First, every builtin ordered type is listed. Second, every entry uses ~ (the tilde), which says "any type whose underlying type is this one." That second detail is what makes Cents, Quantity, and your other named types satisfy Ordered without any extra work.
Compare these two attempts:
constraints.Ordered is the tilde version. That's why a function constrained by it accepts e-commerce types like Cents, Quantity, and Sku without you ever asking.
Here's a tiny in-place sort across Cents, Quantity, and string order names. One generic function handles all three:
sortAsc doesn't care what T is, only that values of T can be compared with <. constraints.Ordered enforces exactly that property at compile time. If you tried to call sortAsc with []bool, the compiler would refuse, because bool doesn't satisfy Ordered.
A common use is a generic max for any ordered type. Here's one that finds the most expensive product in a cart:
T is unconstrained (any) because the function doesn't compare T values directly. It compares the keys returned by key(item), which are constrained to Ordered. This pattern shows up constantly: sort, max, min, group by, and so on all want an Ordered key.
Cost: constraints.Ordered is purely a compile-time constraint. There's no runtime overhead from using it; the generated code is the same as if you had written a non-generic version per type, modulo Go's GC shape stenciling.
constraints.Integer, Signed, UnsignedInteger admits every integer type, signed or unsigned. Signed admits just the signed ones. Unsigned admits just the unsigned ones. The package definitions look roughly like this:
Integer is literally a union of Signed and Unsigned via the | operator at the type set level. That's a feature of constraint interfaces: you can embed other constraint interfaces, and the type set becomes the union.
Which one should you pick?
| Constraint | Use when |
|---|---|
Integer | The function works for any integer type. Counts, indexes, IDs, histogram bins. |
Signed | You need negative values. Differences, deltas, signed offsets. |
Unsigned | You're working with sizes, bit masks, or values that can't be negative by definition. |
Histogram bucketing is a clean fit for Integer. Quantities, rating counts, and similar e-commerce values are non-negative, but the function itself doesn't need to forbid signed types, so Integer is the right scope:
If you had used constraints.Ordered instead, the function body would fail to compile, because / and % aren't defined on string or float. Integer is the right level of strictness here: any arithmetic that requires integer division or remainder.
Signed shows up when negative values are part of the design. A priceDelta that compares last week's price against this week's must produce negatives:
If T were constrained to Unsigned and previous[i] > current[i], the subtraction would wrap around and produce a giant positive number rather than a negative. The constraint here is documentation: this function expects a signed integer because it returns possibly negative results.
Unsigned is rarer in application code, more common in low-level work. A common example is computing a checksum or a bitmap mask. For e-commerce code, the typical use is when a value's domain genuinely cannot be negative, such as a Stock count modeled as uint32:
The choice between Integer and Unsigned here is mild. Integer would also compile. Unsigned documents the invariant that the values being summed cannot be negative. In production, Integer is the safer default unless you have a specific reason to forbid signed types.
constraints.Float and ComplexFloat admits the two builtin floating-point types and their named variants. Complex admits the two builtin complex types. The definitions are short:
Numerical code is the natural home for these. Averaging prices, computing weighted scores, or smoothing a rating series all want Float:
T(len(values)) is the standard pattern: you convert the integer count into your generic float type. Go's type rules require you to do this explicitly; the compiler won't mix T and int for division.
A weighted score function is a slight extension. Here's one that computes a weighted average review score for a product, where each review has a numeric score and a numeric weight (e.g., a verified-buyer review counts more):
Complex is rarely needed in application code. It exists for completeness because complex numbers are built into Go. Signal processing, FFT-style code, and some graphics work use it. A demonstration is the most this lesson needs:
constraints.Complex is listed here for completeness. Most business applications don't use complex numbers.
Cost: Float and Complex are also pure compile-time constraints. Floating-point math itself has costs (rounding, NaN handling), but the constraint doesn't add any.
You can build new constraints by embedding others. The semantics here are precise and worth pinning down.
When a constraint interface embeds two type sets, the resulting type set is the intersection of those sets, not the union. That's how Go's interface satisfaction works: a type must satisfy every embedded interface to satisfy the outer one.
Look at this attempt to write "any integer or float":
The set of types that are both integers and floats is empty. No type satisfies both. A function constrained by IntOrFloat cannot be instantiated with any type, and any attempt produces a compile error.
To get the union (any integer or float), you have to use the | operator inside one interface, not separate embedded lines:
constraints.Integer | constraints.Float is one type set: take the type set of Integer, take the type set of Float, take their union. Now int, int32, float64, and named types like Cents and Rating all satisfy Number.
Here's a full program that defines Number and uses it to sum any numeric slice:
The Number constraint is reusable across the codebase. Plenty of projects define exactly this constraint, often alongside a Real (numeric without complex) or Numeric (numeric including complex) alias.
A subtle case: what if you want "ordered values that are also numeric"? That's where the intersection semantics of multi-line embedding are actually useful:
In this specific case, the intersection equals the union side because everything in Integer | Float is already in Ordered. So OrderedNumber is equivalent to Integer | Float, just spelled differently. Most code prefers the simpler Number form.
The diagram contrasts the two operations. On the right, Number = Integer | Float is the union, which is what you almost always want. On the far right, embedding both as separate lines gives the intersection, which is empty and therefore useless as a constraint.
cmp.Ordered in the Standard LibraryGo 1.21 promoted the most useful piece of constraints into the standard library as cmp.Ordered. Its definition is the same as constraints.Ordered:
The cmp package also ships cmp.Compare[T Ordered](x, y T) int and cmp.Less[T Ordered](x, y T) bool, the building blocks the rest of the stdlib needs for sorting and ordering.
Here's the same minimum example as before, but using cmp.Ordered from the stdlib instead of the x/exp version:
Same behavior, no external dependency. If your project is on Go 1.21 or newer and you only need Ordered, prefer cmp.Ordered.
What about the other constraints? At the time of writing, the stdlib only promoted Ordered (plus a few helpers in cmp). Integer, Float, Signed, Unsigned, and Complex are still only in golang.org/x/exp/constraints. If you need any of those, you still import the x/exp package.
A reasonable rule of thumb:
| Need | Use |
|---|---|
Just Ordered and you're on Go 1.21+ | cmp.Ordered (stdlib) |
| Anything else (Integer, Float, Signed, Unsigned, Complex) | constraints.X (x/exp) |
| Go 1.18-1.20 | constraints.X for everything |
A small program that uses both packages side by side to make the boundary explicit:
clampPrice uses cmp.Ordered because ordering is all it needs. totalUnits uses constraints.Integer because it needs the operator behavior of integers (in this case, just +=, but more generally to forbid floats).
slices and maps Packages (Go 1.21+)The slices and maps packages were promoted into the standard library in Go 1.21. Almost every function in them is generic, and most of the ordering-aware ones use cmp.Ordered. That means you rarely have to write minOf, maxOf, sortAsc, or contains by hand anymore. The stdlib has them.
A short selection of what slices ships:
| Function | What it does | Constraint |
|---|---|---|
slices.Sort | Sorts in place by < | cmp.Ordered |
slices.SortFunc | Sorts using a custom comparator | any element |
slices.Min, slices.Max | Returns smallest/largest | cmp.Ordered |
slices.Contains | Reports whether a value is present | comparable |
slices.Index | Returns the index of a value, or -1 | comparable |
slices.Reverse | Reverses in place | any |
The following program uses several of them on a slice of Product values:
Two interesting bits here. First, slices.SortFunc doesn't need Ordered, because the comparator does the comparing. That's why we can sort Product (which isn't itself Ordered) with it. Inside the comparator, we use cmp.Compare on the Cents field, which is Ordered.
Second, slices.Min and slices.Max work directly on []Cents because Cents satisfies cmp.Ordered. You don't write minOf yourself.
A useful pattern with maps. The maps.Keys function (Go 1.23+ in stdlib, earlier in x/exp) returns an iterator over a map's keys. Here's a quick example that builds a price lookup and then iterates the keys deterministically by sorting them first:
maps.Keys produces an iterator (Go 1.23 iterator type), and slices.Sorted consumes it and returns a sorted slice. The two packages dovetail through generic constraints: slices.Sorted is constrained by cmp.Ordered, and the keys are string, which satisfies it.
Cost: slices.Sort uses pattern-defeating quicksort and is faster than sort.Slice on small to medium slices because it avoids the reflection that sort.Slice needs. For large or hot-path sorts, slices.Sort is the default choice.
Reaching for constraints.Ordered or constraints.Integer is usually right, but a custom type set is sometimes clearer or smaller.
Use a custom constraint when:
int32 and int64 (say, because the algorithm relies on 32-bit-or-wider arithmetic), write interface { ~int32 | ~int64 }. constraints.Integer is too permissive.interface{ String() string }, not by constraints.Ordered. The constraints package is for value types and operators only.Number or Score reads better in documentation than an imported constraints.X. The constraint name communicates intent.golang.org/x/exp/constraints is a single small package, but if your project is otherwise stdlib-only and on Go 1.21+, defining your own Number interface (or using cmp.Ordered) avoids pulling in x/exp.A concrete example. Suppose you're writing a checksum function that XOR-folds 32-bit or 64-bit integers only, because narrower types might cause aliasing issues you want to avoid. constraints.Integer would accept int8 and uint16, which you don't want:
WideInt is custom because the standard Integer is wider than needed. The constraint name documents the rule, the type set enforces it, and the function refuses narrow types at compile time.
Another situation: a generic logger that needs String(). Here, constraints.Ordered would be meaningless. What you want is a method constraint:
The constraint here is a method set, not a type set. constraints.Ordered couldn't express this. Writing a custom interface is the only option, and it's the natural one.
The summary rule: use constraints (or cmp.Ordered) when the function's correctness depends on operators that work the same way across many builtin types. Use a custom constraint when you need a narrower type set, method requirements, or a name that communicates intent.
10 quizzes