skip to content

When do you constrain a Go type parameter with comparable rather than cmp.Ordered, and what does each permit?

level: middleimportance: must knowfreq 62%

answer

  1. equality versus ordering
  2. one is predeclared, the other is imported
  3. map keys need only the first
  4. the ordered one is a union of numbers and strings

basics

~20 s

Use comparable when the code needs equality: it is the predeclared constraint for types supporting == and !=, which is what a map key or set element requires. Use cmp.Ordered when the code needs ordering with <, >: integers, floats and strings.

solid answer

~50 s

`comparable` is predeclared, and its type set is the strictly comparable types — those on which `==` is defined and cannot panic. That is what you need to key a `map[K]V`, de-duplicate into a set, or write `if a == b`. It admits structs and arrays of comparable fields, and excludes slices, maps and functions entirely. `cmp.Ordered`, from the standard library's `cmp` package, is a different thing: a union of every integer, float and string type, each approximated with `~`, so it also admits callers' defined types. It permits `<`, `<=`, `>` and `>=` in addition to equality, which is what a leaderboard or a sort needs. `cmp.Ordered` is strictly narrower in membership but strictly wider in operations. Pick the weakest one the body actually needs: a map key wants `comparable`, a ranking wants `cmp.Ordered`.

code

go · 13 lines
go
type Board[K comparable, V any] struct {
	byKey map[K]V // K comparable is what makes this legal
}

func Highest[E cmp.Ordered](values []E) E {
	best := values[0]
	for _, v := range values[1:] {
		if v > best { // needs cmp.Ordered; comparable would not compile
			best = v
		}
	}
	return best
}

go deeper

for a junior

Remember that a generic map key needs the comparable constraint, and that ordering with < needs cmp.Ordered instead.

for a middle

Explain that comparable is the strictly comparable type set while cmp.Ordered is a tilde-approximated union of numbers and strings, and say which operators each unlocks in the body.

for a senior

Show that you pick the weakest constraint the body needs, and that you know ordering floats through cmp.Compare rather than raw < when NaN is possible.

for a principal

Own the API question: a constraint on an exported generic type dictates what callers may key or rank by, so decide early whether your package promises equality, ordering, or a caller-supplied comparison function.

## Two predeclared-or-standard constraints, two different jobs Go ships two constraints that cover most generic code that touches values rather than just moving them. **`comparable`** is a predeclared identifier — no import, and it is not an ordinary interface you can name as a variable type. Its type set is the **strictly comparable** types: types for which `==` and `!=` are defined and cannot panic. Concretely that is booleans, all numeric types, strings, pointers, channels, arrays of comparable elements, and structs whose fields are all comparable. It excludes slices, maps and functions, and any struct or array containing one of those. **`cmp.Ordered`** lives in the standard `cmp` package, added in Go 1.21 alongside `slices` and `maps`. It is an ordinary constraint interface holding one big union: ```go type Ordered interface { ~int | ~int8 | ~int16 | ~int32 | ~int64 | ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr | ~float32 | ~float64 | ~string } ``` Every term is approximated, so a caller's `type Score int` is a member. ## What each one buys the body | | `comparable` | `cmp.Ordered` | |---|---|---| | `==`, `!=` | yes | yes | | use as a `map` key | yes | yes | | `<`, `<=`, `>`, `>=` | no | yes | | `+` | no | yes (numeric addition or string concatenation) | | structs, arrays, pointers, channels as arguments | yes | no | So they are not a hierarchy in the membership sense — `cmp.Ordered`'s type set is a subset of `comparable`'s — but they are a hierarchy in what the body may do. The general rule for type parameters applies throughout: an operation is legal only when every type in the set supports it with the same meaning. ## Choosing between them Ask what the code does with the value. - **Keying, de-duplicating, membership**: `comparable`. `type Set[T comparable] map[T]struct{}` and `type Board[K comparable, V any] struct { byKey map[K]V }` are the canonical shapes. Using `cmp.Ordered` here would gratuitously reject a struct key. - **Ranking, sorting, min/max, range queries**: `cmp.Ordered`. A leaderboard that keeps the top N scores needs `<`, and only the ordered set supplies it. - **Both**: `cmp.Ordered` already gives equality, so you rarely need to write both. If you need ordering on a type outside the ordered set — a struct with several fields — do not reach for a constraint at all: take a comparison function, the way `slices.SortFunc` does. ## The float edge, and cmp's helpers Ordering floats with bare `<` is not a total order: `NaN < x` and `NaN > x` are both false, so a hand-written sort can loop or produce nonsense. The `cmp` package addresses this: `cmp.Less(x, y)` and `cmp.Compare(x, y)` treat a NaN as smaller than every non-NaN value, giving a total order. `cmp.Compare` returns `-1`, `0` or `+1`, which is exactly the shape `slices.SortFunc` wants. If your generic code sorts `cmp.Ordered` values and floats are possible, prefer those helpers to raw operators. ## What comparable does not promise One subtlety worth knowing before it bites: `comparable` describes *strictly* comparable types, but Go's satisfaction rule lets an ordinary interface type — including `any` — be used as the type argument even though comparing two interface values can panic when the dynamic type is a slice. So `comparable` guarantees the operation compiles, not that every possible instantiation is panic-free. For code that must not panic, narrow the constraint to a union of concrete-ish terms such as `~string | ~int64`. ## Practical shape ```go type Board[K comparable, V any] struct { byKey map[K]V } func Highest[E cmp.Ordered](values []E) E { best := values[0] for _, v := range values[1:] { if v > best { best = v } } return best } ``` The two constraints sit side by side in one small package and never overlap in purpose: one identifies things, the other ranks them.

  • Why are slices rejected by comparable?
    Go defines no `==` for slice, map or function types — only comparison against `nil` — so they are not comparable at all and sit outside the type set. Structs and arrays containing such a field are excluded for the same reason. Key on a string form, or take a hash or equality function instead.
  • What does cmp.Compare return, and how does it handle NaN?
    It returns `-1`, `0` or `+1` for less, equal and greater, and it treats a NaN as less than any non-NaN value so that ordering floats stays total. Raw `<` cannot do that: every comparison with NaN is false, which can break a hand-rolled sort.
  • How would you order values whose type is not in cmp.Ordered, such as a struct?
    Do not push it into the constraint. Take a comparison function parameter — `func(a, b E) int` in the shape `slices.SortFunc` uses — and constrain the element with `any`. Constraints express what the language can do with a type; anything richer belongs in a function value.

saying these in an interview costs you the question

  • Says comparable allows < because comparing includes ordering
  • Thinks cmp.Ordered includes bool, structs or time values
  • Constrains a map key parameter with any
  • Believes comparable accepts a slice if its elements are comparable