Why is a map entry stored under math.NaN() unreachable in Go, and how do you get rid of it?
answer
- the key does not match itself
- float64 is comparable, so it compiles
- len grows while lookups return nothing
- delete compares keys the same way
- one builtin empties a map without comparing
basics
~20 sA float64 NaN is never equal to anything, itself included, so lookup and delete never match the entry. Each insert adds another one and len keeps growing; only clear or a fresh map removes them.
solid answer
~40 s`float64` is a comparable type, so `map[float64]T` compiles and `m[math.NaN()] = v` is accepted. A map lookup hashes the key and then compares candidates with `==`, and IEEE 754 says every comparison involving NaN is false — including NaN against itself. So the insert never finds an existing entry to overwrite and appends a new one, `m[math.NaN()]` returns the zero value with `ok` false, and `delete(m, math.NaN())` is a no-op. `len(m)` grows with every insert, and ranging over the map is the only way to see the entries at all, though deleting by the ranged key fails for the same reason. `clear(m)` empties the map without comparing keys, so it is the escape hatch; the real fix is `math.IsNaN` at the boundary so a NaN never becomes a key.
code
go · 7 linesm := map[float64]string{}
m[math.NaN()] = "first"
m[math.NaN()] = "second"
_, ok := m[math.NaN()]
delete(m, math.NaN())
fmt.Println(len(m), ok)
// prints: 2 falsego deeper
Remember the headline: NaN is not equal to itself, so a map cannot find an entry keyed by it, and you test for NaN with math.IsNaN rather than with ==.
Explain the mechanics: a lookup hashes and then compares with ==, that comparison is always false for NaN, and therefore inserts accumulate while reads and deletes miss. Know that clear empties the map.
Demonstrate how you would catch this in production — a map whose length grows while the code writes one computed key, confirmed by logging math.Float64bits, and fixed by validating at the single write site.
Frame the design rule you would hold teams to: computed floats should not be identity. Decide where keys are canonicalised, and whether the codebase keys on exact scaled integers so the failure cannot occur at all.
This is one of the few places where two independent Go rules combine into behaviour that looks like a bug and is not. ### The map accepts the key `float64` supports `==`, so `map[float64]string` is a legal map type and the compiler has nothing to complain about. Nothing in the type system distinguishes a float64 that happens to be NaN from any other float64 — `math.NaN()` returns a perfectly ordinary value of type `float64`. ### The lookup can never match it A Go map find is two steps: hash the key to locate a bucket, then compare the candidate keys in that bucket with `==` to pick the right one. IEEE 754 defines NaN as unequal to everything, itself included, so the second step can never succeed for a NaN key. Three consequences follow, and they are all visible in a few lines: - **Insert always adds.** `m[math.NaN()] = "a"` finds no existing entry to overwrite, so it appends a new one. Do it twice and `len(m)` is 2. Do it in a loop and the map grows without bound while looking, from the outside, like it holds one key. - **Read always misses.** `v, ok := m[math.NaN()]` gives the zero value and `ok == false`, even though the entry is sitting there. - **Delete always misses.** `delete(m, math.NaN())` compares keys exactly as a lookup does, so it removes nothing. Even `for k := range m { delete(m, k) }` fails for these entries: the key handed to you by the loop is a NaN, and comparing it back against the stored NaN is still false. Ranging over the map does yield the entries — iteration walks storage and never compares keys — so you can *see* a NaN entry and read its value, you just cannot address it. In the gc runtime NaN keys are also hashed non-deterministically, so repeated inserts scatter rather than piling into one bucket. ### Removing them `clear(m)` deletes every entry in a map without comparing anything, so it removes NaN entries along with the rest. Rebuilding the map by ranging and copying the keys you want also works, since you never have to match a NaN. There is no way to delete a single NaN entry by key. ### The neighbouring surprise: negative zero While you are here, the other float64 key oddity runs the opposite way. `-0.0` and `0.0` compare equal, so they are *one* key, not two — a value stored under negative zero is found by looking up positive zero. `math.Float64bits` shows they are different bit patterns and `math.Signbit` tells them apart, but the map cannot, because the map only knows `==`. And in Go a constant `-0.0` is simply `0`; you need `math.Copysign(0, -1)` or negating a variable to produce a real negative zero. ### What to do in real code A float64 key is usually a smell in the first place, but when you want one: 1. Reject or normalise at the boundary. `if math.IsNaN(k) { return err }` in the one function that writes to the map is the whole fix, and it costs nothing. 2. Prefer a key that cannot be NaN. Money and quantities keyed as scaled integers (minor units, basis points) are exact, comparable and deletable; a float64 derived from a division is none of those reliably. 3. Treat an unexpectedly growing map as evidence. A map whose `len` climbs while the code only ever writes "the same" computed key is the signature of this bug, and printing `math.Float64bits` of that key in the log confirms it instantly — a NaN shows all exponent bits set with a non-zero fraction, which no ordinary value has. The underlying rule is worth stating once in plain terms: Go maps promise you lookup by equality, and NaN opts itself out of equality. Everything above is that single sentence played out.
- How do you test whether a float64 is NaN in Go?`math.IsNaN(v)`. It is implemented as `v != v`, which is true only for NaN, and it is the only correct test: `v == math.NaN()` is false for every value because NaN compares false against everything. Pair it with `math.IsInf(v, 0)` when you want to reject any non-finite result.
- Do positive zero and negative zero act as two different map keys?No, they are one key: `-0.0 == 0.0` is true, so a value stored under one is found under the other. `math.Float64bits` shows the patterns differ and `math.Signbit` distinguishes them, but the map only consults `==`. Note that the Go constant `-0.0` is just `0` — producing a real negative zero needs `math.Copysign(0, -1)`.
- What would you use instead of a float64 map key when NaN can occur?Either validate with `math.IsNaN` at the single write site and return an error, or key by something exact: scaled integers such as minor units or basis points, or a canonical string. A key derived from a division is the risky case, since that is where zero over zero enters.
It is a parcel filed under a name that matches nobody, not even itself: the shelf holds it, the index can never find it, and only emptying the whole shelf clears it out.
saying these in an interview costs you the question
- Says the compiler rejects float64 as a map key type
- Believes delete removes a NaN key like any other key
- Thinks NaN equals NaN because the bit patterns match
- Assumes len stays at one since it is the same key
- Detects NaN by comparing a value against math.NaN()