How do Go's float verbs and strconv.ParseFloat handle NaN and the infinities?
answer
- three values that are not digits
- precision has nothing to print
- the plus sign is part of the token
- one of them is never equal to itself
- JSON has no spelling for either
basics
~20 sGo prints them as NaN, +Inf and -Inf under every float verb, ignoring precision. strconv.ParseFloat accepts NaN, Inf and Infinity case-insensitively, so the text round-trips — but NaN never equals itself, and encoding/json refuses to marshal either.
solid answer
~50 sBoth `fmt` and `strconv` treat them as named values rather than numbers. Whatever the verb — `%f`, `%e`, `%g` or `%v` — a not-a-number prints as `NaN` and the infinities as `+Inf` and `-Inf`, and precision is ignored, so `%.2f` of positive infinity is `+Inf`; width still pads. Going the other way, `strconv.ParseFloat` recognises `NaN`, and `Inf` or `Infinity` with an optional sign, matching case-insensitively, so `inf` and `+INFINITY` both parse. A parse of a well-formed but too-large literal like `1e999` returns an infinity together with a range error, which is a different situation from parsing the word. Two traps follow: `NaN != NaN`, so a verification test must call `math.IsNaN` rather than compare values; and `encoding/json` rejects both with an unsupported-value error, so these values die at the JSON boundary, not at the formatting one.
code
go · 8 linesfmt.Printf("%f %.2f %g\n", math.NaN(), math.Inf(1), math.Inf(-1))
// NaN +Inf -Inf
f, err := strconv.ParseFloat("infinity", 64)
// err == nil, math.IsInf(f, 1) is true
g, err := strconv.ParseFloat("NaN", 64)
// err == nil, math.IsNaN(g) is true, and g == g is falsego deeper
Recall the three literal outputs: NaN, +Inf and -Inf, printed the same under every float verb. Know that math.IsNaN exists because a NaN is not equal to itself.
Explain that formatting special-cases these values before any digits are generated, so precision is ignored while width still pads, and that strconv.ParseFloat accepts NaN, Inf and Infinity case-insensitively with an optional sign.
Show where they actually break a service: encoding/json refuses them and fails the whole document, and an out-of-range parse returns an infinity plus an error that is easy to drop. Validate at the ingestion edge rather than at serialisation.
Own the schema answer: whether a numeric field may be absent, whether these values are rejected at the boundary or represented explicitly, and who is told when a sensor feed starts producing them.
## They are printed as names, not numbers A `float64` has three kinds of value that are not ordinary finite numbers: positive infinity, negative infinity, and not-a-number. Go's formatting code special-cases all of them before any digit generation happens. Under `%f`, `%e`, `%g`, `%v` and `strconv.FormatFloat` with any format byte, the output is one of exactly three strings: ``` fmt.Printf("%f %.2f %g\n", math.NaN(), math.Inf(1), math.Inf(-1)) // NaN +Inf -Inf ``` Note what the precision did: nothing. `%.2f` did not produce `+Inf.00`. **Width, however, still applies**, because width is about the field, not the digits: `%8.2f` of positive infinity yields four spaces followed by `+Inf`. That is enough to keep a fixed-width report column aligned, and enough to surprise a parser that expected `[0-9.]+`. The positive infinity always carries its `+`. This is deliberate and it matters when the consumer is a regular expression or a schema validator: the token is `+Inf`, not `Inf`. ## Parsing accepts more spellings than it produces `strconv.ParseFloat` recognises the string `NaN`, and the possibly-signed strings `Inf` and `Infinity`, ignoring case. So all of `nan`, `NaN`, `inf`, `+Inf`, `-INFINITY` parse successfully with a nil error. The set of accepted spellings is wider than the set produced, which is the usual robustness asymmetry — it means text produced by another language's formatter (`inf`, `Infinity`) is usually readable. Separately, `ParseFloat` returns an infinity for input that is *numerically* out of range: a syntactically valid literal such as `1e999` yields `+Inf` **together with a range error**. Those two cases look identical if you ignore the error and identical again if you only inspect the value, so code that must distinguish "the source said infinity" from "the source said a number too big to hold" has to look at both the returned value and the returned error. ## The equality trap in the verification test A round-trip check is the natural way to prove a format is lossless, and the naive version is wrong for exactly one input: ``` back, _ := strconv.ParseFloat(strconv.FormatFloat(f, 'g', -1, 64), 64) if back != f { t.Fatal("did not round-trip") } // fires on every NaN ``` `NaN != NaN` by definition, so this reports a failure that is not one. Branch instead: ``` if math.IsNaN(f) { if !math.IsNaN(back) { t.Fatal("NaN lost") } return } if back != f { t.Fatal("did not round-trip") } ``` Swapping in raw bit comparison with `math.Float64bits` does not rescue it either: a NaN has many bit patterns (its payload), and the NaN produced by parsing the text `NaN` need not carry the payload of the NaN you formatted. Comparing bits is right for distinguishing `+0` from `-0`, which do compare equal with `==` but are different values; it is wrong for NaN. `math.IsInf(f, 1)` and `math.IsInf(f, -1)` are the corresponding checks for the infinities, though those do compare equal to themselves, so `==` works for them. ## The real boundary is JSON, not fmt The practical failure is rarely in the text format — it is one layer out. `encoding/json` cannot represent these values, because the JSON number grammar has no spelling for them. Marshalling a struct or map containing one fails with an unsupported-value error naming the value, and it fails for the whole document, not just the field. So a pipeline that formats fine with `fmt` will still refuse to emit the record, usually deep inside a handler, and usually with the offending field's name absent from the message. The options, none of them free, are: reject or repair the value at ingestion (a division that produced NaN is nearly always a bug worth failing on); model the field as a `*float64` and encode absence as `null`; or carry the value as a string field whose contract explicitly allows `NaN` and `+Inf`. Choosing at the schema level is far cheaper than discovering it when one sensor divides by zero. ## Where the values come from `math.NaN()` and `math.Inf(sign)` construct them explicitly, but in real data they arrive by accident: a `0.0/0.0` between two variables, an average over zero samples, an overflow from repeated multiplication, a parse of an out-of-range literal, or a conversion from another system that spells absence as an infinity. Because they propagate silently — any arithmetic touching a NaN yields a NaN, and comparisons with one are all false — they usually surface far from their origin, at exactly the moment something tries to write them out. Validating at the edge, with `math.IsNaN` and `math.IsInf`, converts a mysterious serialisation error into a clear rejection with the input in hand.
- How should a round-trip test assert that a NaN survived formatting and parsing?Branch on `math.IsNaN` for both the original and the parsed value, and only use `==` on the non-NaN path. Comparing with `==` reports a false failure because `NaN != NaN`; comparing `math.Float64bits` also fails, because the parsed NaN need not carry the original payload bits.
- What happens when a struct containing an infinity is passed to json.Marshal?It fails with an unsupported-value error — JSON's number grammar has no representation for NaN or the infinities — and the whole document is refused, not just that field. Validate at ingestion, or model the field as a pointer encoding absence as null, or agree a string contract for it.
- strconv.ParseFloat returned +Inf. Did the input say 'Inf' or was it out of range?Check the error. Parsing the word `Inf` (or `infinity`, any case) returns an infinity with a nil error. A syntactically valid but too-large literal such as `1e999` also returns an infinity, but with a range error alongside it. The value alone cannot tell the two apart.
saying these in an interview costs you the question
- Expects %.2f of an infinity to print +Inf.00
- Thinks fmt prints special values as 0 or as an empty string
- Compares a parsed NaN with == and blames strconv
- Assumes encoding/json can serialise NaN
- Believes ParseFloat rejects the string inf
- Treats a +Inf result from ParseFloat as proof the text said Inf