comparable and cmp.Ordered — Professional Level¶
Table of Contents¶
- API design with
comparablevscmp.Ordered - Why
cmp.Orderedexcludes complex numbers - The Go 1.21 sortable shift
- Library patterns: keys, weights, totals
- Compatibility windows for downstream users
- Method-based ordering vs operator ordering
- Generic vs interface dispatch — when each wins
- Case study:
slicesandmaps - Case study: hashicorp/golang-lru/v2
- Case study: a metrics library
- Team guidelines and review checklist
- Migration checklist
- Summary
API design with comparable vs cmp.Ordered¶
A library author chooses between three constraint families:
| Constraint | Says to caller | Use when |
|---|---|---|
[T any] | "Bring any type" | The body never compares values |
[T comparable] | "Bring an equality-friendly type" | Map keys, deduplication, Has/Index |
[T cmp.Ordered] | "Bring an orderable type" | Sort, heap, range queries, BST |
Loosening the constraint expands the user base. Tightening it expands the operations the body can perform. A senior library author picks the smallest constraint that lets the implementation work — never tighter than needed, never looser than safe.
A worked design¶
Suppose you are building a Top[T] helper that returns the N largest elements:
- If you write
func Top[T any](s []T, n int, less func(T, T) bool) []T, callers must always supplyless. - If you write
func Top[T cmp.Ordered](s []T, n int) []T, callers do not — but they cannot pass structs. - The right choice is two functions:
Top(cmp.Ordered) andTopFunc(any with comparator). This mirrors the stdlibslices.Sort/slices.SortFuncsplit.
Rule of thumb: provide the convenient form, then the general form. Do not force every caller to write a comparator if the type already supports <.
Why cmp.Ordered excludes complex numbers¶
complex64 and complex128 are conspicuously absent from cmp.Ordered. The reason is not laziness — it is a design choice.
A complex number a + bi has two real components. There is no canonical total order. Three options exist:
- Lexicographic — compare real parts, then imaginary
- Magnitude — compare
|a + bi| - Argument — compare
arg(a + bi)
Each is mathematically valid but none is universal. In financial code you might want magnitude. In numerical code you might want lexicographic. In control theory you might want argument.
If cmp.Ordered included complex numbers, the spec would have to pick one — and any choice would silently break code that expected another. So complex was excluded. Programmers who need complex ordering must:
slices.SortFunc(cs, func(a, b complex128) int {
return cmp.Compare(cmplx.Abs(a), cmplx.Abs(b)) // magnitude order
})
The exclusion forces the choice to be explicit at every call site. That is the right outcome for a feature whose meaning depends on context.
What about user-defined types?¶
Even with the tilde, a complex underlying does not satisfy cmp.Ordered. There is no escape hatch.
The Go 1.21 sortable shift¶
Before Go 1.21, sorting was an interface{}-based mess:
sort.Slice uses reflection internally. Slow, allocs, no type safety. Or you wrote a sort.Interface with Len/Less/Swap — three methods of boilerplate per type.
Go 1.21 promoted slices.Sort and friends:
import (
"cmp"
"slices"
)
slices.Sort(ages) // for []int directly
slices.SortFunc(users, func(a, b User) int { // for structs
return cmp.Compare(a.Age, b.Age)
})
Three changes to your mental model after 1.21:
- Inlinable comparator —
slices.Sortknows the type, can inline<. Often 40% faster thansort.Slice. - No reflection — bug class eliminated.
- NaN handling —
slices.Sort(which usescmp.Compare) gives a deterministic NaN order.sort.Slicedid not.
The Go team expects new code to use slices.Sort / slices.SortFunc and treats sort.Slice as legacy. Linters in 2024+ flag sort.Slice as a hint to migrate.
Why this matters for cmp.Ordered¶
slices.Sort is parameterised as:
So calling slices.Sort(myDuration) works because time.Duration's underlying type is int64. Calling slices.Sort(myStruct) does not. The constraint is the gate.
Library patterns: keys, weights, totals¶
A useful taxonomy of generic uses:
| Role | Constraint | Typical signature |
|---|---|---|
| Key | comparable | Cache[K comparable, V any] |
| Weight / score | cmp.Ordered | Top[T cmp.Ordered](s []T, n int) []T |
| Total / accumulator | custom Number | Sum[T Number](s []T) T |
| Tag | comparable (often string) | Set[T comparable] |
| Index / page | Ordered integers | Range[T cmp.Ordered](lo, hi T) Iter[T] |
When you reach for a constraint, ask: "Is this thing a key (equality), a weight (ordering), or a value (arithmetic)?". The answer picks the constraint.
Compatibility windows for downstream users¶
A library that uses cmp.Ordered requires Go 1.21+. A library that uses comparable works on Go 1.18+ (with relaxed semantics in 1.20+).
Implications for go.mod:
| Constraint used | Min go directive |
|---|---|
any, comparable (strict) | go 1.18 |
comparable (with interface arguments) | go 1.20 |
cmp.Ordered, cmp.Compare | go 1.21 |
cmp.Or | go 1.22 |
| Generic type aliases | go 1.24 |
A library targeting older toolchains imports golang.org/x/exp/constraints.Ordered instead of cmp.Ordered. The two are equivalent in practice; the type is constraints.Ordered and is API-stable.
A senior library author declares the minimum Go in go.mod deliberately, knowing each line above costs you some users.
Method-based ordering vs operator ordering¶
Two patterns coexist:
Operator ordering — cmp.Ordered¶
Works for primitive-shaped types. Not extensible to user struct types.
Method ordering — interface¶
type Comparable[T any] interface {
Compare(other T) int
}
func MinM[T Comparable[T]](a, b T) T {
if a.Compare(b) < 0 { return a }
return b
}
Works for any struct that implements Compare. Extensible. Slightly more boilerplate at call site.
Hybrid — comparator function¶
Most flexible. Caller supplies the rule. Used by slices.SortFunc, slices.MinFunc, etc.
When to expose which¶
- Convenience overload —
Min[T cmp.Ordered]for the common case - Extensible overload —
MinFunc[T any]for user-defined order - Method-based — only if the domain already has a
Comparemethod (rare)
Stdlib slices follows this exact pattern: Sort / SortFunc, Min / MinFunc, BinarySearch / BinarySearchFunc.
Generic vs interface dispatch — when each wins¶
The classic question: when should a library use [T cmp.Ordered] vs an interface like sort.Interface?
| Aspect | Generics | Interfaces |
|---|---|---|
| Performance | inlinable, no v-table | dynamic dispatch, slower |
| Composition | hard — types are different | easy — interface satisfaction |
| Reflection | not needed | sometimes needed |
| API stability | tighter constraint = breakage risk | loose interface, easier to extend |
| User experience | infer or specify T at call site | implement methods on the type |
For algorithms over value-typed data (numbers, strings, simple structs), generics win. For algorithms over polymorphic data (mixed renderers, mixed loggers), interfaces win.
Sort is a corner case: it is value-shaped (the elements are homogeneous) but historically used sort.Interface. Go 1.21 picked generics — and benchmarks justified it.
Case study: slices and maps¶
The standard library's adoption pattern is worth studying.
slices.Contains¶
Two type parameters: S for the slice type (so user-defined slice types like type Names []string work), E for the element. The constraint on E is comparable — equality only. No ordering required for Contains.
slices.Index¶
Same constraint, same shape — equality is enough.
slices.Sort¶
Constraint on E is cmp.Ordered — sort needs <.
slices.BinarySearch¶
Binary search needs ordering, so cmp.Ordered.
slices.Compact¶
Compact removes adjacent duplicates — equality only.
The pattern: Go uses comparable whenever it can, and bumps to cmp.Ordered only when ordering is required. Senior library design follows the same rule.
maps¶
Map equality needs both K comparable (already required by maps) and V comparable (so values can be compared with ==). For non-comparable values, callers use maps.EqualFunc.
Case study: hashicorp/golang-lru/v2¶
When Hashicorp shipped their generic LRU cache in /v2, they had to pick constraints:
K comparable because LRU uses an internal map[K]*entry. V any because values are stored without comparison. They did not add cmp.Ordered because the LRU policy uses recency, not value ordering.
Compare with a hypothetical SortedCache[K cmp.Ordered, V any] that maintains keys in sorted order — that one would need cmp.Ordered for the BST-like backbone.
The lesson: constraints follow what the implementation does, not what the user thinks the type "is".
Case study: a metrics library¶
Suppose you build a percentile calculator:
type Quantile[T cmp.Ordered] struct {
samples []T
}
func (q *Quantile[T]) Add(v T) { q.samples = append(q.samples, v) }
func (q *Quantile[T]) P(p float64) T {
sorted := slices.Clone(q.samples)
slices.Sort(sorted)
idx := int(float64(len(sorted)) * p)
return sorted[idx]
}
Why cmp.Ordered? The implementation sorts. Why not [T any] with a comparator? You could — but the convenience form is much nicer when the user is measuring time.Duration or int.
A library that wraps this would expose both:
type Quantile[T cmp.Ordered] struct { ... }
type QuantileFunc[T any] struct {
samples []T
cmp func(T, T) int
}
Team guidelines and review checklist¶
Adopt these in your style guide:
- Default to
[T comparable]for keys; use[T cmp.Ordered]only when the body sorts or orders.- Never re-define
Ordered; importcmp.Ordered(Go 1.21+) orconstraints.Ordered(older).- Sort comparators on float types must use
cmp.Compare, never<.- Public APIs must declare the minimum Go version in
go.modconsistent with their constraints.- For non-Ordered struct types, expose a
Comparemethod and aSortFunc-style helper.- Document NaN behavior on any API that takes floats.
- Watch for
[T comparable]overinterface{}/any— the runtime panic risk should be either documented or guarded.
Review checklist¶
| Check | Why |
|---|---|
Is comparable enough, or is Ordered really needed? | Looser constraint = more callers |
| Does the comparator handle NaN? | Float bugs are hard to detect |
Are user-defined types handled (~T)? | Domain types often have wrappers |
Is there a Func variant? | Users with non-Ordered types need it |
| Is the minimum Go version declared? | cmp.Ordered requires 1.21+ |
Migration checklist¶
For a team migrating to cmp.Ordered:
- Bump
go.modtogo 1.21or newer - Replace
golang.org/x/exp/constraints.Orderedimports withcmp.Ordered - Replace hand-rolled
Orderedinterfaces in internal packages - Replace
sort.Slice(s, func(i, j int) bool { return s[i] < s[j] })withslices.Sort(s) - Replace
<withcmp.Comparein float sort comparators - Use
cmp.Orfor tie-breaking chains (Go 1.22+) - Audit
Set[any]andCache[any, V]for runtime panic risk - Add NaN tests for any float-touching code
- Document constraint choice in package godoc
Summary¶
comparable and cmp.Ordered are the two constraints that drive the majority of generic Go code. The professional view of them:
- Constraint follows implementation, not type identity. A cache uses
comparableeven if the keys "feel" ordered. comparableis the minimum-equality contract — relaxed in 1.20 to include interfaces, with runtime panic risk.cmp.Orderedis the canonical ordering constraint — predeclared-in-spirit, closed to specific underlying types, NaN-aware viacmp.Compare.- Operators are NaN-blind;
cmp.Compareis NaN-aware. Senior code routes throughcmp.Comparefor floats. - Two-form APIs win. Provide the Ordered convenience form and the comparator form side by side, like
slices.Sort/slices.SortFunc. - Migration to 1.21 is worthwhile —
slices.Sortis faster, NaN-safe, and inlines. - Complex numbers are excluded by design. Their ordering is context-dependent, so the spec refuses to pick.
A team that internalizes these rules writes generic Go that is small, fast, and predictable across version boundaries. The next file (specification.md) digs into the formal grammar and the spec sections that govern these constraints.