comparable and cmp.Ordered — Senior Level¶
Table of Contents¶
- Why
cmp.Orderedexists - The exact definition
- Why
~intand friends — domain types matter - Floating-point semantics — NaN and the strict-weak-order trap
cmp.Comparevscmp.Lessvs the operators- User-defined types satisfying
Ordered - Building generic ordered data structures
- Stable ordering and ties
- Sorting time-of-day, durations, and currency
- Summary
Why cmp.Ordered exists¶
For three years (Go 1.18-1.20), every team that wanted generic ordering had to write their own constraint. The community converged on something close to golang.org/x/exp/constraints.Ordered:
type Ordered interface {
~int | ~int8 | ~int16 | ~int32 | ~int64 |
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr |
~float32 | ~float64 |
~string
}
When cmp was promoted to the standard library in Go 1.21 (August 2023), the team made cmp.Ordered the canonical version. Hand-rolled Ordered constraints became an anti-pattern overnight.
The package also added:
These are the NaN-aware sort helpers. They give a deterministic ordering even when floats include NaN. Operators (<, <=) do not — they simply return false for NaN comparisons.
The exact definition¶
From cmp/cmp.go in the standard library:
// Ordered is a constraint that permits any ordered type:
// any type that supports the operators < <= >= >.
// If future releases of Go add new ordered types,
// this constraint will be modified to include them.
//
// Note that floating-point types may contain NaN values,
// for which the operator <, <=, >, >= return false even
// when compared with themselves. See [Compare] for a
// consistent way to compare NaN values.
type Ordered interface {
~int | ~int8 | ~int16 | ~int32 | ~int64 |
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr |
~float32 | ~float64 |
~string
}
Three observations a senior must internalize:
~everywhere — every term uses tilde, so user-defined types whose underlying type is one of these qualify.- No complex numbers —
complex64andcomplex128are absent. Complex numbers do not have a meaningful total order (Re vs Im — which dimension wins?). The Go team excluded them deliberately. - The constraint is open-ended in spec text — "if future releases add new ordered types, this will be modified." So depending on
cmp.Orderedis a forward-compatible decision.
Why ~int and friends — domain types matter¶
Consider this code:
type Celsius float64
type Fahrenheit float64
func Min[T cmp.Ordered](a, b T) T {
if a < b { return a }
return b
}
var t1, t2 Celsius = 36.6, 38.2
Min(t1, t2) // 36.6 — works because cmp.Ordered uses ~float64
Without the tilde, Celsius would be rejected — its underlying type is float64 but the named type itself is Celsius. The tilde is what makes generic numeric code usable in domain code (temperature, currency, distance, age, score).
This is also why a hand-rolled Ordered interface that uses bare int | float64 | string is broken — domain types like type UserID int will not satisfy it. Always include ~.
When the tilde rule bites¶
type Score int
scores := []Score{3, 1, 2}
slices.Sort(scores) // ✓ — slices.Sort takes [S ~[]E, E cmp.Ordered]
fmt.Println(scores) // [1 2 3]
Most stdlib generics use ~T-shaped constraints internally so domain types just work.
Floating-point semantics — NaN and the strict-weak-order trap¶
IEEE-754 says: NaN != NaN, and NaN < x, NaN > x, NaN <= x, NaN >= x all return false. This breaks two assumptions sorting algorithms make:
- Trichotomy — for any
a,b, exactly one ofa < b,a == b,a > bholds. NaN breaks this — none holds for NaN. - Total order — for any
a,b,c: ifa < bandb < c, thena < c. NaN breaks transitivity.
A slices.Sort over []float64{1, math.NaN(), 2} historically produced indeterminate results — sometimes correct, sometimes a partial sort, occasionally infinite loop in older sorts. The Go 1.21 fix:
slices.Sort([]float64{1, math.NaN(), 2})
// NaNs are sorted to the front (treated as less than all numbers)
slices.Sort and slices.SortFunc use cmp.Compare internally, which does define a total order:
cmp.Compare(x, y)returns -1 ifx < y, 0 ifx == y, +1 ifx > y. For floats, NaN is less than any non-NaN value, and any two NaNs are equal.
So cmp.Compare extends the operator-level < with deterministic NaN handling. Operators do not. Senior rule: never use < on floats inside a sort comparator. Always go through cmp.Compare or cmp.Less.
The pattern¶
import (
"cmp"
"slices"
)
type Reading struct {
Sensor string
Value float64
}
slices.SortFunc(readings, func(a, b Reading) int {
return cmp.Compare(a.Value, b.Value)
})
This sorts NaN readings to the front predictably.
cmp.Less¶
cmp.Less(NaN, 1.0) == true, cmp.Less(1.0, NaN) == false, cmp.Less(NaN, NaN) == false. This is the predicate version of the same logic.
cmp.Compare vs cmp.Less vs the operators¶
Three ways to ask "is a less than b?":
| Tool | Returns | NaN-safe? | When to use |
|---|---|---|---|
a < b (operator) | bool | no — false for any NaN | when no NaN can occur |
cmp.Less(a, b) | bool | yes — NaN is "less" | predicates, conditions |
cmp.Compare(a, b) | int (-1, 0, 1) | yes | sort comparators |
Use the operator only when you have proven the values cannot be NaN. Use cmp.Less for predicates and cmp.Compare for sort.
A common mistake is to write a sort comparator with <:
slices.SortFunc(s, func(a, b T) int {
if a < b { return -1 } // ❌ broken for NaN
if a > b { return 1 }
return 0
})
If T is float64 and any element is NaN, this comparator violates total order. Replace with return cmp.Compare(a, b).
User-defined types satisfying Ordered¶
Any defined type whose underlying type is in cmp.Ordered's union qualifies, thanks to ~:
type UserID int64
type Currency float64
type Email string
type Priority uint8
// All four satisfy cmp.Ordered without further work:
slices.Sort([]UserID{...})
slices.Sort([]Currency{...})
slices.Sort([]Email{...})
slices.Sort([]Priority{...})
Types that do not satisfy:
type ID struct { Hi, Lo uint64 } // struct — not in Ordered
type Bag []string // slice — not in Ordered
type Time time.Time // struct — not in Ordered (time.Time is comparable but not Ordered)
For these, you write a custom comparator and use slices.SortFunc.
Wrapping a non-Ordered type to make it Ordered¶
type Ordered ID = func(a, b ID) int { ... } // not allowed
// Instead: provide a Compare method and use SortFunc.
func (a ID) Compare(b ID) int {
if c := cmp.Compare(a.Hi, b.Hi); c != 0 { return c }
return cmp.Compare(a.Lo, b.Lo)
}
slices.SortFunc(ids, func(a, b ID) int { return a.Compare(b) })
You cannot bolt cmp.Ordered onto a struct, even with methods. The constraint is closed to the listed underlying types. This is by design — the spec does not allow user interfaces to extend cmp.Ordered.
Building generic ordered data structures¶
A senior writes data structures parameterised by cmp.Ordered:
Sorted slice¶
type SortedSlice[T cmp.Ordered] struct {
data []T
}
func (s *SortedSlice[T]) Insert(v T) {
i, _ := slices.BinarySearch(s.data, v)
s.data = slices.Insert(s.data, i, v)
}
func (s *SortedSlice[T]) Has(v T) bool {
_, ok := slices.BinarySearch(s.data, v)
return ok
}
Min-heap¶
type Heap[T cmp.Ordered] struct {
data []T
}
func (h *Heap[T]) Push(v T) { /* heapify-up using cmp.Compare */ }
func (h *Heap[T]) Pop() T { /* swap, heapify-down */ }
BST¶
type BST[T cmp.Ordered] struct { root *node[T] }
type node[T cmp.Ordered] struct {
v T
left *node[T]
right *node[T]
}
For all three, the constraint cmp.Ordered says "this T is totally orderable" — exactly the requirement of binary search, heap, and tree.
When to use comparable instead¶
If a structure only needs equality (set, map-like cache, dedup), comparable is enough:
type Set[T comparable] struct { m map[T]struct{} }
type Cache[K comparable, V any] struct { m map[K]V }
Choosing comparable over cmp.Ordered when ordering is not needed keeps the API loose — callers do not need to provide an Ordered type.
Stable ordering and ties¶
slices.Sort is not stable. When two elements compare equal, their relative order is unspecified. slices.SortStableFunc is stable.
For cmp.Ordered-based sorting:
// Unstable
slices.Sort(s)
// Stable, with explicit comparator
slices.SortStableFunc(s, cmp.Compare)
If your data has natural ties (e.g., users with the same age), use stable sort or include a tie-breaker:
slices.SortFunc(users, func(a, b User) int {
if c := cmp.Compare(a.Age, b.Age); c != 0 { return c }
return cmp.Compare(a.ID, b.ID) // tie-breaker
})
cmp.Or (Go 1.22+) makes this concise:
slices.SortFunc(users, func(a, b User) int {
return cmp.Or(
cmp.Compare(a.Age, b.Age),
cmp.Compare(a.ID, b.ID),
)
})
cmp.Or returns the first non-zero result — exactly what a chained comparator needs.
Sorting time-of-day, durations, and currency¶
time.Time is comparable (a struct of integers and a pointer to location), but not Ordered in the cmp sense — its underlying type is a struct. So:
Workaround:
slices.SortFunc(times, func(a, b time.Time) int {
return a.Compare(b) // time.Time has its own Compare method since 1.20
})
Same story for time.Duration — but wait, time.Duration is int64 underneath:
So duration sorts work directly. Currency types defined as type USD int64 or type Cents int64 also satisfy cmp.Ordered automatically.
A pattern for "almost-ordered" types¶
When a type is conceptually ordered but its concrete shape is a struct (time.Time, big.Int, decimal.Decimal), provide a Compare method and use SortFunc. Do not try to redefine cmp.Ordered.
Summary¶
cmp.Ordered is the canonical constraint for "supports <, <=, >, >=". Three rules a senior remembers:
- Use it as-is, do not redefine. It is closed to its listed underlying types and uses tilde everywhere.
- Operators are NaN-blind;
cmp.Compareandcmp.Lessare NaN-aware. Usecmp.Comparein every sort comparator that touches floats. - Complex numbers are excluded. Real, integer, and string ordering — yes. Complex — no. Take the constraint as a positive design choice, not an oversight.
Patterns that age well:
- Generic data structures (
SortedSlice,Heap,BST) parameterised by[T cmp.Ordered] - Sort comparators that route through
cmp.Compareand chain viacmp.Or - Domain types
type X int,type Y float64,type Z stringthat ride free on~T - Equality-only structures (
Set,Cache) staying on[T comparable]to keep the constraint loose
The pair comparable + cmp.Ordered covers almost every algorithmic generic in Go. The rest of the constraint system (custom unions, ~, methods) builds on top, but comparable and Ordered are the workhorses.
Move on to professional.md to see how teams design libraries around these two constraints.