skip to content

How does Go's cmp.Compare order a NaN float64, and what does it return for two NaNs?

level: middleimportance: should knowfreq 28%

answer

  1. IEEE says false in both directions
  2. the package has to fill two holes
  3. NaN gets a home at one end
  4. and is declared equal to itself
  5. signed zeros are left as one value

basics

~10 s

cmp.Compare treats a NaN as smaller than every non-NaN value, so NaNs come first, and reports two NaNs as equal by returning 0. Negative and positive zero also compare equal, giving a total order.

solid answer

~40 s

`cmp.Compare` deliberately fixes the holes IEEE-754 leaves. A NaN is treated as less than any non-NaN, so `cmp.Compare(math.NaN(), 1.0)` is `-1` and NaNs collect at the front of an ordered result. Two NaNs are treated as equal, so `cmp.Compare(math.NaN(), math.NaN())` is `0` — even though `math.NaN() == math.NaN()` is false. Negative zero and positive zero compare equal, giving `0`. `cmp.Less` follows the same rules. The point is that these three decisions turn `float64` into a **total order**: every pair has a defined relationship, and equality is reflexive and transitive. With the raw operators, every comparison against a NaN is false in both directions, which means a hand-written comparator silently reports "equal" for pairs that are not, and any ordering built on it becomes input-dependent.

code

go · 6 lines
go
negZero := math.Copysign(0, -1)

fmt.Println(cmp.Compare(math.NaN(), 1.0))        // -1: NaN is below every number
fmt.Println(cmp.Compare(math.NaN(), math.NaN())) // 0: two NaNs count as equal
fmt.Println(cmp.Compare(negZero, 0.0))           // 0: signed zeros count as equal
fmt.Println(math.NaN() == math.NaN())            // false: the operator says otherwise

go deeper

for a junior

Remember the headline: with floats, comparison operators answer false in both directions for NaN, so a plain comparison cannot order them. Knowing cmp.Compare defines an answer is enough here.

for a middle

Be able to state all three rules — NaN below everything, NaN equal to NaN, signed zeros equal — and explain why the raw operator's false-in-both-directions behaviour is what forces them to be written down.

for a senior

Connect the rule to consequences: a comparator that leaves NaN undefined produces results that change with input order, and the fix belongs both in the comparator and at the boundary where NaN entered the data.

for a principal

Decide the NaN policy once, in the package that owns the ordering, and document it, so every importing team inherits the same answer instead of each one re-deriving a different rule at its own call sites.

## The problem cmp.Compare is solving IEEE-754 floating point has values that break the assumptions an ordering usually rests on: - **NaN** (not-a-number, produced by `0.0/0.0`, `math.Sqrt(-1)`, a failed parse that was defaulted, `math.NaN()`). Every comparison involving a NaN is false: `x < NaN`, `x > NaN`, `x == NaN` and even `NaN == NaN` are all false. - **Negative zero**, the distinct bit pattern produced by `math.Copysign(0, -1)` or by underflow from below. It is a different value from positive zero at the bit level, yet `-0.0 == 0.0` is true. If you write the obvious comparator by hand — ```go if a < b { return -1 } if a > b { return 1 } return 0 ``` — then any pair containing a NaN falls through both tests and is reported as **equal**. That is not merely imprecise, it is self-contradictory: a NaN is then "equal" to 1, and "equal" to 2, while 1 and 2 are not equal to each other. Equality has stopped being transitive, and an ordering built on a non-transitive equality has no defined result. ## The three rules cmp applies `cmp.Compare` and `cmp.Less` define exactly the missing cases: 1. **A NaN is less than any non-NaN.** So NaNs sort to the front, deterministically, rather than landing wherever the algorithm happens to leave them. 2. **A NaN is equal to a NaN.** `cmp.Compare(math.NaN(), math.NaN())` returns `0`, unlike the `==` operator. 3. **Negative zero equals positive zero.** `cmp.Compare(math.Copysign(0, -1), 0.0)` returns `0`, matching the `==` operator and refusing to invent an order between them. These apply only to the floating-point members of `cmp.Ordered`; for integers and strings the functions agree with the plain operators in every case. ## Why "total order" is the right phrase A comparison function is usable for ordering only if it is *consistent*: for every pair it gives one answer, the answer does not depend on which other elements are around, equality is reflexive (`x` equals itself) and transitive, and less-than is transitive and antisymmetric. The raw `<` operator over `float64` fails reflexivity the moment a NaN appears — `NaN < NaN` is false, `NaN == NaN` is false, so a NaN is neither less than, greater than, nor equal to itself. By declaring NaN to be the smallest value and equal to itself, `cmp` restores every one of those properties. Any ordering routine driven by `cmp.Compare` therefore produces the same result for the same multiset of inputs, no matter what order they arrived in. ## What this does not do It does not make NaN a *sensible* value in your data. Putting NaNs first is a defined behaviour, not a desirable one: a leaderboard whose top rows are all NaN is still a data-quality bug. The right production posture is usually to detect them with `math.IsNaN` at the boundary where the values are parsed or computed, report or reject the rows, and let `cmp.Compare` be the safety net that guarantees a defined result rather than the place where the problem is handled. It also does not distinguish negative zero. If your domain genuinely needs `-0.0` to be a separate value — a physics or signal-processing corner — you must compare bit patterns with `math.Signbit` or `math.Float64bits` yourself; `cmp` deliberately follows the `==` operator here. ## Seeing it ```go negZero := math.Copysign(0, -1) cmp.Compare(math.NaN(), 1.0) // -1 cmp.Compare(1.0, math.NaN()) // +1 cmp.Compare(math.NaN(), math.NaN()) // 0 cmp.Compare(negZero, 0.0) // 0 math.NaN() == math.NaN() // false — the operator disagrees ``` The last line is the one to remember: the whole reason to route float comparisons through `cmp.Compare` is that it and the `==` operator answer differently for NaN, and only one of the two answers yields a usable ordering. ## A note on maps and equality The NaN-equals-NaN rule is local to `cmp`. It does not change how a `map[float64]V` behaves, where a NaN key is unreachable because the map uses the `==` operator, and it does not change what a deep-equality helper does. If you need a NaN-aware equality check over two float slices, `cmp.Compare(a[i], b[i]) == 0` is the cheap way to express it, precisely because it treats NaN as equal to NaN.

  • Why does cmp.Compare declare NaN equal to NaN when the == operator does not?
    Because an ordering needs a reflexive, transitive equality. If a NaN were unequal to itself, a value could not even be found in a sequence containing it, and equality would depend on which comparison happened to run. Declaring the case is the only way to get a defined, repeatable result.
  • Does this mean you can safely leave NaN values in production data?
    No. It guarantees a defined order, not a meaningful one — NaNs simply collect at the front. Detect them at the boundary with `math.IsNaN`, reject or report the rows, and treat `cmp.Compare`'s rule as the safety net that stops a bad value from corrupting the whole result.
  • How would you compare two float64 slices treating NaN as equal to NaN?
    Walk them in parallel and test `cmp.Compare(a[i], b[i]) == 0` after checking the lengths match. That reuses the package's NaN rule directly. The `==` operator would report the two NaNs as different and make the check fail on identical data.

Think of it as writing a house rule for the two cards a standard deck does not cover: the jokers all rank below every numbered card and count as identical to each other. The rule is arbitrary, but once it is written down every deal sorts the same way.

saying these in an interview costs you the question

  • Says NaN sorts last, or wherever it happens to land
  • Claims cmp.Compare panics or errors on NaN
  • Thinks NaN == NaN is true in Go
  • Believes negative zero sorts before positive zero
  • Assumes the raw < operator is equally safe for floats