AlgoMaster Logo

The cmp Package

Low Priority22 min readUpdated June 6, 2026

The cmp package, added in Go 1.21, gives you a generic vocabulary for ordering values. It defines the Ordered constraint that slices.Sort and friends rely on, a pair of comparison functions that handle floating-point NaN cleanly, and (since Go 1.22) cmp.Or for picking the first non-zero value out of a list. The package is small, with three top-level functions and one constraint, but it cleans up a class of code that used to require either reflection or hand-written Less functions for every type. Most production Go projects pick it up the day they upgrade past 1.21, because it removes boilerplate from every sort and every "pick a default" expression.

Why a Comparison Package?

Before Go 1.21, writing generic code that compared values was awkward. You either accepted a Less func(a, b T) bool callback at every call site or you wrote a type-specific helper. Sorting a slice of integers, a slice of floats, and a slice of strings each needed its own boilerplate, even though the comparison is the same idea.

The cmp package solves this by exposing the set of types that Go's < operator already works on, integers, floats, and strings, as a single constraint named cmp.Ordered. Once you have that constraint, you can write generic functions that work for any orderable type, and the standard library has a uniform place to spell "this type supports <".

The diagram lays out the relationship. cmp.Ordered is not a type, it's a constraint, an interface used at compile time to restrict which types a generic function will accept. Anything whose underlying type is an integer, float, or string satisfies it. Pointers, slices, structs, and channels do not.

Here's the constraint's actual shape, simplified for readability:

The ~int means "any type whose underlying type is int". So a custom type type Price int also satisfies cmp.Ordered. That tilde is what lets your domain types ride along on the same machinery.

A first taste of the package, sorting product prices:

This is the whole package showing up at once: cmp.Compare returns a three-way result (-1, 0, or +1), and slices.SortFunc consumes that result. We'll unpack each piece in the next sections.

cmp.Compare and cmp.Less

The package's two core functions are cmp.Compare and cmp.Less. Both work on any cmp.Ordered type.

FunctionSignatureReturns
cmp.Comparefunc Compare[T Ordered](x, y T) int-1 if x < y, 0 if equal, +1 if x > y
cmp.Lessfunc Less[T Ordered](x, y T) booltrue if x < y

cmp.Compare is the standard tool. The three-way result is what sorts and binary searches expect, and it composes cleanly when you need to break ties on a second field (more on that below). cmp.Less is convenience: it returns just a bool, useful when you're feeding a predicate into something that wants Less rather than Compare.

String comparison is lexicographic, the same byte-by-byte ordering you'd get from < on strings. "notebook" comes before "pen" because 'n' < 'p'.

The NaN Subtlety

Floating-point comparison has a corner case that is a common issue out. NaN (not-a-number) is not equal to anything, including itself, and the < and > operators return false in both directions. That breaks normal sorts: a slice containing a NaN can end up partially sorted with the NaN floating wherever, and worse, the same input can sort differently on different runs.

cmp.Compare handles this. Its rule is that NaN is less than any non-NaN value, and two NaNs are equal to each other. That makes the function a total order, which is what sorting algorithms actually need.

The first three lines show the standard operators all returning false. The next three show cmp.Compare giving sensible answers: NaN is treated as smaller than the real number, and equal to itself. This is the version you want when you're sorting product ratings that might contain a malformed value, or when you don't fully trust your input data.

cmp.Compare and cmp.Less are tiny generic functions, the compiler typically inlines them, so there's no overhead versus writing a < b by hand. Use them freely.

cmp.Or for Default Fallbacks

Go 1.22 added cmp.Or, a generic function that returns the first non-zero value from its arguments.

The function walks its argument list in order and returns the first one that isn't the zero value for T. If every argument is zero, it returns the zero value. This is useful any time you want a "try in priority order" pattern: read from a request, fall back to a config file, fall back to a hard-coded default.

The constraint is comparable, not Ordered. That's broader, it includes booleans, pointers, channels, interfaces, and structs whose fields are all comparable. The reason is that cmp.Or only needs to check whether a value equals the type's zero value, which uses ==, not <.

A common shape in e-commerce code is reading a user-supplied value with a fallback chain. Before cmp.Or, you'd write a nested ternary-style helper or a series of if statements. Now it's one line.

Each field follows its own fallback chain: request first, then user profile, then defaults. The empty strings act as the "not set" signal, and cmp.Or picks the first non-empty one.

One thing to watch out for: cmp.Or evaluates all of its arguments before the call, like any regular Go function. If the fallback value is expensive to compute, build it lazily yourself with an if. cmp.Or(req, expensiveDefault()) will run expensiveDefault() every time, even when req is non-zero.

The "zero value" check is exactly what == produces against the type's zero. For numeric types, that's 0. For strings, it's "". For booleans, it's false. For pointers, channels, and interfaces, it's nil. For structs, the zero value is the struct with every field set to its own zero value. This last case can surprise you: a struct that happens to have all default fields will be skipped, even though you may have constructed it deliberately:

deliberatelyEmpty matches the zero value for Address, so cmp.Or skips it and picks the next one. If "empty but present" is meaningful to your domain, you can't represent that distinction with cmp.Or alone. Use a *Address (pointer) instead so that nil means "not present" and &Address{} means "explicitly empty".

cmp.Or does a linear scan of its arguments. It's fine for two, three, or five values. Chaining ten suggests a different pattern is needed.

Sorting Custom Types

cmp.Compare becomes interesting when you sort structs. slices.SortFunc takes a function with the signature func(a, b T) int, which is exactly what cmp.Compare returns. The pattern is to pick the field you want to sort by and forward it to cmp.Compare.

The sort is in place. slices.SortFunc rearranges the slice itself, no copy. If you need to preserve the original order, copy first with slices.Clone.

To sort descending instead, swap the arguments:

That flips every -1 into +1 and vice versa, which is exactly what reverse order is.

Binary Search by a Field

slices.BinarySearchFunc uses the same comparison pattern. The slice must already be sorted by the same key you're searching on.

The comparator's two arguments have different types: the slice element is a Product, and the search target is a float64. The function returns the comparison of the product's price against the target price. This is what BinarySearchFunc (as opposed to plain BinarySearch) lets you do, search a slice of one type using a key of a different type.

slices.BinarySearchFunc is O(log n) and assumes the slice is already sorted. Sorting first costs O(n log n), so if you binary-search a slice once and then throw it away, you'd have been better off with a linear scan. The break-even point is roughly when you'll search the same sorted slice more than a few times.

Multi-Key Sorting with cmp.Or

cmp.Or is useful on the comparison side. When you want to sort by one field and break ties on another, you need a comparator that returns the first field's result if it's not zero, and falls back to the second field's result if it is. This is what cmp.Or does for int values.

The list is sorted by price first. Among the three items at $1.49 and the two at $4.99, the names break the tie alphabetically. The pattern reads top to bottom in priority: try price; if it's a tie (i.e. cmp.Compare returned 0), use name.

You can chain as many levels as you want. Sorting orders by status, then by total, then by customer name:

Status comes first ("pending" before "shipped" lexicographically). Within each status, total ascends. Within ties on total, customer name decides. And if every other field is equal, the ID acts as a final stable tiebreaker. That last tier matters: without it, two orders with the same status, total, and customer would be in undefined relative order. Adding ID as the deepest tiebreaker makes the sort fully deterministic.

The flowchart shows what cmp.Or is doing internally. Each cmp.Compare produces a result; if it's non-zero, that becomes the answer; if it's zero, the chain moves to the next comparison. The short-circuit is what makes this efficient, later comparisons are skipped once an earlier one has decided the order.

Stable Sorts and Why They Matter Here

slices.SortFunc does not promise to preserve the original order of elements that compare as equal. That matters when you're sorting on a partial key. Consider a list of orders already in arrival order, and you want to sort them by Status. Two orders with the same status could end up in either relative order, and a later view of the data might show them differently from one run to the next.

If you need to preserve the input order for tied keys, use slices.SortStableFunc. It has the same signature as SortFunc but guarantees that equal elements stay in their input positions.

Within each status group, the original input order is preserved. With plain SortFunc, those orderings would not be guaranteed.

The other way to get a deterministic sort, the one we already saw in the multi-key example, is to add a unique field (typically an ID) as the deepest tiebreaker. That makes every pair of elements distinguishable, so there are no "equal" elements left for stability to matter. Pick whichever fits your data: SortStableFunc when you have an existing meaningful order to preserve, or a unique tiebreaker when you don't.

SortStableFunc is slightly slower than SortFunc and uses more memory because the underlying algorithm has to track positions. Avoid it by default, use it when you need the order guarantee.

Working with Custom Ordered Types

The ~ in cmp.Ordered's definition matters when you have domain types that wrap a primitive. A type Price float64 or type CustomerID int is its own type at the Go type system level, but its underlying type is a float or int, so it's still orderable.

cmp.Compare(a.Spend, b.Spend) works here because Price's underlying type is float64, which satisfies cmp.Ordered. You don't have to convert with float64(a.Spend), the constraint handles it directly. This is the small but important quality-of-life win that the ~ in the constraint gives you: your domain types ride along on the same machinery as the primitives they wrap.

If you ever do need to cross between two named types whose underlying types match but are declared differently, convert explicitly:

Practical Patterns from E-Commerce Code

A few patterns come up over and over in real catalog and order-management code. Here are three that pull the whole cmp package together.

Pattern 1: Top-N by Score

You want the top five best-selling products. The naive approach sorts the whole list and takes the first five. That works, and for small catalogs it's fine. The combination of slices.SortFunc and a slice operation is the most readable solution:

slices.Clone is important here. Without it, topN would mutate the caller's slice. The comparator reverses the arguments (b, a instead of a, b) to sort descending. The bounds check at the end keeps the function safe when the caller asks for more results than there are products.

Sorting the whole catalog is O(n log n). For very large catalogs and a small N, a partial sort or a min-heap of size N can be faster, but for catalogs of a few thousand items, the sort is fine and simpler to read.

Pattern 2: Filter Then Sort

A common reporting query is "show me all orders over $50, sorted by date". The simplest way is to filter into a new slice, then sort that slice:

Filter first, then sort the smaller slice. That's usually faster than sorting the whole list and then filtering, because sorting is O(n log n) while filtering is O(n). Make sort work on the smaller input.

Pattern 3: Compose Comparators

When you have several different views over the same data ("by price ascending", "by rating descending", "by name alphabetical"), it's useful to define each comparator separately and then compose them on demand. Functions are values in Go, so this is straightforward:

The compose helper does exactly what cmp.Or(cmp.Compare(...),...) does inline, but lets you mix and match named comparators. This is useful when the comparators encode business rules ("active products before discontinued ones") that you want to reuse across multiple sort calls.

Note that byRating sorts descending (the swap is inside the function), while byPrice and byName sort ascending. The composed result respects each one's direction.

When Not to Use cmp

A few practical limits to remember.

cmp.Compare doesn't work on slices, maps, structs, or interfaces directly. Those types aren't Ordered. If you want to sort structs, you extract an Ordered field and compare on that, which is the whole multi-key pattern above. If you want to sort by something computed (length of a string, distance from a target), pull the value out into a local variable first or call a helper:

Sorting by case-insensitive string requires lowercasing both sides yourself; cmp.Compare is byte-by-byte, so "Apple" sorts before "banana" because uppercase letters have smaller byte values than lowercase ones. If that's wrong for your data, normalize first:

For locale-aware sorting (so "é" sorts near "e" and not way down at the end of ASCII), use golang.org/x/text/collate. The standard cmp and slices packages don't do Unicode collation.

Finally, cmp.Or is comparable, not Ordered. You can cmp.Or two booleans or two *User pointers, but the operation is "first non-zero", not "first less than". If you confuse the two, you'll get surprising results.

GoalUse
Three-way compare of two orderable valuescmp.Compare
Boolean less-than of two orderable valuescmp.Less
First non-zero value from a listcmp.Or
Sort a slice of orderable typesslices.Sort (no comparator needed)
Sort a slice of structs by one fieldslices.SortFunc + cmp.Compare
Sort by multiple fieldsslices.SortFunc + cmp.Or + multiple cmp.Compare

Quiz

cmp Package Quiz

10 quizzes