Why does strconv.FormatFloat with precision -1 round-trip exactly through ParseFloat?
answer
- minus one is not a digit count
- the fewest digits nobody else shares
- the parser rounds to the nearest float
- one string, one possible float64
- the last argument names the population
basics
~20 sPrecision -1 makes strconv.FormatFloat emit the fewest digits that no other float64 shares, so strconv.ParseFloat reads the string back as the identical value. Any fixed precision can print a string that parses back as a different number.
solid answer
~50 s`strconv.FormatFloat(f, 'g', -1, 64)` asks for the smallest number of digits that uniquely identifies `f` among all `float64` values. Because no neighbouring float shares that decimal string, `strconv.ParseFloat(s, 64)` — which rounds correctly to the nearest float64 — must land back on the same bit pattern. The guarantee holds for every finite value and for the infinities; it is the reason `%v` output can be re-read safely. A fixed precision has no such property: `%.2f` of two distinct floats can produce the same string, and `%.17f` can produce a string whose nearest float is a different value than you started with for very small or very large magnitudes. The `bitSize` argument picks which set of values must be distinguished: pass 32 for a value that came from a `float32`, so the shortest string round-trips as a `float32`.
code
go · 12 linesa, b := 0.1, 0.2
f := a + b
s := strconv.FormatFloat(f, 'g', -1, 64)
// s == "0.30000000000000004"
back, _ := strconv.ParseFloat(s, 64)
// back == f
lossy := strconv.FormatFloat(f, 'f', 2, 64)
// lossy == "0.30"
other, _ := strconv.ParseFloat(lossy, 64)
// other != fgo deeper
Know that strconv.FormatFloat takes a precision argument and that passing -1 means 'as many digits as needed and no more', and that this is what fmt.Println already does for floats.
Explain why shortest-unique implies an exact round trip: the string falls inside one float's rounding interval, and ParseFloat rounds to nearest. Be able to say what the bitSize argument selects and why 32 gives a shorter string.
Demonstrate the judgment about output contracts: choose shortest-unique for anything a program re-reads, choose a fixed scale only where the scale is the defined contract, and know that NaN needs math.IsNaN in the verification test.
Own the rule across services: which numeric fields are reconstructible by contract, where rounding is allowed to happen exactly once, and how a change of output precision is treated — as a breaking schema change rather than a formatting tweak.
## The signature and the sentinel ``` func FormatFloat(f float64, fmt byte, prec, bitSize int) string func ParseFloat(s string, bitSize int) (float64, error) ``` `prec` is normally a digit count — places after the decimal point for `'f'` and `'e'`, significant digits for `'g'`. The special value **-1** means something categorically different: *use the smallest number of digits necessary to represent the value uniquely*. ## Why "uniquely" implies a round trip A `float64` is one of a finite set of binary values. Each one owns an interval of real numbers that round to it. "Shortest unique" means: emit the shortest decimal string that falls inside this value's interval and inside no neighbour's. `ParseFloat` is a correctly-rounded parser — it returns the float64 nearest to the decimal string. Feed it a string that lies in exactly one float's interval and it must return that float. So for every finite `f`: ``` back, err := strconv.ParseFloat(strconv.FormatFloat(f, 'g', -1, 64), 64) // err == nil and back == f, bit for bit ``` This is a *guarantee*, not a statistical property, and it is why the shortest form is the right choice for any text that a program will read back: JSON fixtures, CSV exports, log lines a parser consumes, golden files in tests. ## Why a fixed precision cannot promise this A fixed precision is a lossy projection. Two different float64 values can format to the same `%.2f` string, and once they do, no parser can recover which one you had. Going the other way, printing *more* digits than necessary is harmless for correctness but not a guarantee you can reason about by eye: 17 significant digits are always enough for float64, but `%.17f` counts places *after the point*, not significant digits, so it prints garbage precision for large magnitudes and nothing at all for tiny ones. `%.17g` does round-trip, at the cost of ugly output like `0.10000000000000001`. Shortest-unique gives you `0.1` and the same guarantee. ## bitSize is about which neighbours matter The last argument to `FormatFloat` is **not** a request to convert the value. `f` is always a `float64`. `bitSize` says which population the string must be unique within: - `64` — unique among float64 values. - `32` — unique among float32 values, i.e. the caller promises `f` was obtained by converting a `float32`, and the shorter string is fine because float32 neighbours are further apart. So a value that came from a `float32` should be formatted with `bitSize` 32: `float64(float32(0.1))` formatted at 64 gives `0.10000000149011612`, formatted at 32 gives `0.1`. Both are correct for their population; using 64 on a float32-derived value prints noise digits that were never measured. `ParseFloat`'s `bitSize` is symmetric: with 32 it rounds to float32 precision and returns the result widened to `float64`, so `float32(v)` is exact. The round-trip pair for float32 data is `ParseFloat(FormatFloat(f, 'g', -1, 32), 32)`. ## Format byte still matters for the shape Precision -1 works with `'f'`, `'e'` and `'g'` alike — it controls how many digits, not which layout. `'g'` is the usual choice because it also picks a compact layout. `'f'` with -1 keeps plain decimal notation and still round-trips, which is handy when a downstream parser dislikes exponents. There is also `'x'`, hexadecimal floating point (`0x1.999999999999ap-04`), which is exact by construction and independent of decimal rounding entirely — useful in tests and bug reports. ## The values that need special handling The infinities format as `+Inf` and `-Inf` and parse back to the same infinities, so they round-trip fine. Negative zero formats as `-0` and parses back with the sign bit set. **NaN is the exception you must code around**: it formats as `NaN` and parses back to a NaN, but `NaN != NaN`, so a test written as `if back != f { fail }` reports a false failure on every NaN. Compare with `math.IsNaN` on both sides instead. Comparing raw bits with `math.Float64bits` is also not right for NaN, because the parsed NaN need not carry the original payload bits. ## Where this shows up in real code `fmt`'s `%v` on a float is `%g` at default precision, which is the same shortest-unique rule — so `fmt.Sprint(f)` is already round-trippable, and `encoding/json` marshals float64 the same way. The moment you reach for `%.2f` or `%.6f` in a path that another program parses, you have chosen a fixed decimal contract, and it is worth being deliberate about it: fine for a currency column whose scale is defined, wrong for a measurement you intend to reconstruct. ## Cost Shortest-unique formatting is not free — it does real work to find the minimal digit string — but it is a well-optimised path in `strconv` and a rounding error you cannot debug costs far more. If a hot loop formats millions of floats, `strconv.AppendFloat` into a reused byte slice avoids the allocation of a fresh string per value.
- What does the bitSize argument to strconv.FormatFloat change?Which set of values the string must be unique within, not the type of the input — `f` is always a `float64`. Pass 32 for a value that came from a `float32` and you get `0.1` instead of `0.10000000149011612`, because float32 neighbours are further apart and fewer digits separate them.
- Does a round trip through strconv.FormatFloat with precision -1 work for NaN and the infinities?Textually yes: they format as `NaN`, `+Inf` and `-Inf`, and `strconv.ParseFloat` accepts all three. The infinities compare equal after the trip. NaN does not — `NaN != NaN` — so a test must branch on `math.IsNaN` rather than compare with `==` or with raw bits.
- When is a fixed precision the right choice despite being lossy?When the fixed scale is the contract rather than an accident: a currency column defined to two decimals, a report a human reads, a schema that states its precision. The rule is to round once, at the source, so the stored value already is the value you print — not to round on the way out of a value you still consider authoritative.
saying these in an interview costs you the question
- Thinks precision -1 means the platform default digit count
- Believes %.17f round-trips any float64
- Says bitSize 32 converts the value to a float32
- Assumes any fixed precision is safe if it is large enough
- Compares a round-tripped NaN with == and calls it a bug in strconv