- Book: The Complete Guide to Go Programming
- Also by me: Thinking in Go (2-book series) — Complete Guide to Go Programming + Hexagonal Architecture in Go
- My project: Hermes IDE | GitHub — an IDE for developers who ship with Claude Code and other AI coding tools
- Me: xgabriel.com | GitHub
Open the config loader of any Go service older than two years. You'll find this:
port := os.Getenv("APP_PORT")
if port == "" {
port = cfg.Port
}
if port == "" {
port = "8080"
}
Or this, on the sort path:
sort.Slice(orders, func(i, j int) bool {
if orders[i].CreatedAt.Equal(orders[j].CreatedAt) {
return orders[i].ID < orders[j].ID
}
return orders[i].CreatedAt.After(orders[j].CreatedAt)
})
Both shapes work. Both are the kind of thing a code reviewer skims past because the alternative used to be worse. Then Go 1.21 shipped the cmp package with cmp.Compare and cmp.Less. Go 1.22 added cmp.Or. The alternative stopped being worse.
The two functions are tiny. The patterns they unlock are not.
What landed in which release
A short version, because the migration question comes up.
- Go 1.21 (August 2023): the
cmppackage itself, withcmp.Ordered(the constraint),cmp.Compare, andcmp.Less. - Go 1.22 (February 2024):
cmp.Or.
If you are on 1.20, none of this exists in the standard library. If you are on 1.21, you have Compare and Less but not Or. A handful of the patterns below depend on Or doing the heavy lifting. The release-note pages on go.dev document both.
cmp.Compare — the three-way primitive
The signature is exactly what you expect:
func Compare[T cmp.Ordered](x, y T) int
Returns -1 when x < y, 0 when equal, +1 when x > y. Three-way comparison, the C-style strcmp shape. Finally available without writing the eighteen-line if/else-if/return chain by hand.
The point of three-way comparison is not the function itself. It's that it composes. Two-way comparators (bool) chain through nested if-statements. Three-way comparators chain through cmp.Or.
cmp.Less is the same idea returning a bool, useful when an API specifically wants a less-than predicate. Compare is the one you reach for most.
A small but real edge case: floats. cmp.Compare defines a total order, which means NaN is less than everything else and equal to itself. < and > do not. If your data has any chance of NaN and you sort with < directly, you get unstable orders. cmp.Compare(a, b) gives you a stable order. That alone is a reason to prefer it over hand-rolled comparators in any code that touches user-supplied numbers.
cmp.Or — first-non-zero, generalized
The signature is even smaller:
func Or[T comparable](vals ...T) T
Returns the first argument that is not the zero value of T. If all of them are zero, returns the zero value. That is the entire definition.
The first thing it replaces is the config-loading staircase from the top of the post:
port := cmp.Or(
os.Getenv("APP_PORT"),
cfg.Port,
"8080",
)
Three sources, fallback order left to right, one expression. The pattern works for any comparable type: strings, ints, pointer values, struct types whose zero value is meaningful, anything you can compare with ==.
There's a wart worth knowing. cmp.Or compares against the zero value, not nil-or-empty in the general sense. For string that's "", which is what you want. For int that's 0, which means cmp.Or(userSuppliedRetries, 3) returns 3 when the user explicitly asked for zero retries. If zero is a legal user value, cmp.Or is the wrong tool. You need a *int or a "set" flag. Same gotcha with time.Duration defaults. Read the type's zero behavior before reaching for Or.
Pattern 1: configuration with a clear precedence chain
This is the most common use and the one that makes the package pay for itself in the first afternoon.
type Config struct {
DatabaseURL string
Port string
LogLevel string
}
func loadConfig(env Env, fileCfg Config) Config {
return Config{
DatabaseURL: cmp.Or(
env.DATABASE_URL,
fileCfg.DatabaseURL,
"postgres://localhost:5432/app",
),
Port: cmp.Or(env.PORT, fileCfg.Port, "8080"),
LogLevel: cmp.Or(
env.LOG_LEVEL,
fileCfg.LogLevel,
"info",
),
}
}
The shape reads top to bottom: env wins, then file, then default. Adding a fourth source (say a feature-flag service) is one more argument. No new branch. The comparison with the staircase pattern is not subtle: ten lines collapse to one per field, and the precedence is in the order of arguments instead of buried in the order of if blocks.
What it does NOT replace is validation. cmp.Or("", "", "") returns "". If empty is invalid for the field, you still need to check the result. The package gives you fallback, not enforcement.
Pattern 2: multi-key sorting that reads like the spec
This is where cmp.Compare and cmp.Or work together, and where the API earns its keep.
You have a paginated API for orders. Sort key: most recent first, ties broken by lowest ID first. The pre-cmp version was a chained ternary or a nested-if comparator. The post-cmp version is exactly the spec, in code:
type Order struct {
ID int64
CreatedAt time.Time
Total int
}
slices.SortFunc(orders, func(a, b Order) int {
return cmp.Or(
b.CreatedAt.Compare(a.CreatedAt), // desc by date
cmp.Compare(a.ID, b.ID), // asc by ID
)
})
time.Time already has a three-way Compare method (added in 1.20), which is why we don't need cmp.Compare for the date. The b.CreatedAt.Compare(a.CreatedAt) flips the sign so newer-is-first.
cmp.Or returns the first non-zero result. If two orders have different timestamps, the date comparison decides and the ID comparison is never evaluated. If they share a timestamp, the date comparison returns zero and cmp.Or falls through to the ID comparison. Short-circuit evaluation, no manual nesting.
Adding a third tiebreaker (say, total descending) is one more line:
slices.SortFunc(orders, func(a, b Order) int {
return cmp.Or(
b.CreatedAt.Compare(a.CreatedAt),
cmp.Compare(b.Total, a.Total),
cmp.Compare(a.ID, b.ID),
)
})
Three keys, three lines, and the order of the lines is the order of the sort priorities. Most Go code did not have it for sorting, for years.
Pattern 3: paginated cursor-based queries
The reason multi-key sort matters in production is that cursor pagination is built on it. If you paginate by (created_at DESC, id ASC), every page query needs to filter WHERE (created_at, id) < (cursor.created_at, cursor.id) and order the same way. The Go side of that comparison — the test that decides whether a row sits before or after the cursor — is the same cmp.Or chain you just wrote for the sort.
func afterCursor(o Order, c Cursor) bool {
return cmp.Or(
o.CreatedAt.Compare(c.CreatedAt),
cmp.Compare(o.ID, c.ID),
) < 0
}
The function reads as the same data dependency the SQL has. When the sort changes (product asks to add a tiebreaker on customer-priority) you change one place. Not two places. The pre-cmp version drifted between the SQL ORDER BY and the Go-side cursor check. The bug only showed up at page boundaries when two rows shared a timestamp.
Pattern 4: stable IDs from optional fields
You have an event with three possible identifiers and want a stable, non-empty ID for logging. Whichever the upstream sent, in priority order:
func eventID(e Event) string {
return cmp.Or(
e.IdempotencyKey,
e.RequestID,
e.TraceID,
fmt.Sprintf("synthesized-%d", time.Now().UnixNano()),
)
}
The synthesized fallback is the safety net so the function never returns empty. The rule is visible in one glance instead of reconstructable from four if statements.
When NOT to reach for cmp.Or
The line a lot of teams get wrong is treating cmp.Or as a general fallback operator. It is not. It compares against the zero value of T, full stop. The cases it does not cover:
Pointers where
niland "explicit zero" both matter.cmp.Or((*int)(nil), &x)returns&x, which is what you want.cmp.Or(&zero, &x)returns&zero. Both pointers are non-nil, the first wins, the function never reads what they point at. If the rule is "non-nil pointer to a non-zero value," you need a helper, notOr.Slices, maps, channels. They are not
comparable.cmp.Orwill not compile. The stdlib has nothing for "first non-empty slice" — write a three-line helper if you need it.Anything where zero is a legal user choice.
cmp.Or(retries, 3)silently overrides the user'sretries=0. The fix is the explicit-set pattern: a pointer field, anomitempty-aware decoder, or a(value, ok)shape.
The mental model: cmp.Or is the typed, generalized coalesce for cases where zero means unset. Anywhere "zero means unset" is true for your type, it works. Anywhere it isn't, reach for something else.
A short decision rule
The two functions answer two questions. Match the question, the function picks itself.
-
"Which of these candidates do I use?" →
cmp.Orover a list, zero means unset. -
"How do these two values order?" →
cmp.Comparereturning the three-wayint. -
"How do these two records order, by multiple keys?" →
cmp.Orofcmp.Comparecalls, one per key, in priority order.
The pattern that ties them together is the one to internalize: three-way comparators chain through cmp.Or. Two-way comparators do not. That is the reason both functions ship in the same package, and the reason the whole shape only fully arrived in 1.22 with Or filling in the gap.
If this was useful
The cmp package is one of the shorter chapters in The Complete Guide to Go Programming, but the patterns above show up across most chapters — sort keys in collections, fallback chains in config loaders, three-way comparison in custom types. The book covers what each piece of the standard library actually does and which of your habits from other languages translate. Hexagonal Architecture in Go picks up on the architectural side, including how comparison and ordering decisions belong inside the domain layer rather than scattered across adapters.

Top comments (0)