Floating-Point Formatting
Reference index · Summary of this topic
Go’s default rendering of a float — %v, %g, and the bare Println/Print/Sprint paths — is
strconv.FormatFloat(f, 'g', -1, bitSize): the shortest decimal digits that round-trip back to the same
float, laid out in 'e' form when the decimal exponent is below -4 or at/above 6, and 'f' form
otherwise. The exponent is lowercase, always signed, and always at least two digits.
.NET’s default/"R" formatting also produces shortest-round-trip digits, but presents them differently:
it flips to exponent form on its own thresholds and writes an unpadded, uppercase exponent. The two agree
far more often than they disagree, which is what makes the disagreement easy to miss — the gap only opens
at the ends:
fmt.Println(1000000.0) // Go: 1e+06 .NET default: 1000000
fmt.Println(1e-5) // Go: 1e-05 .NET default: 1E-05
fmt.Println(2.2250738585072014e-308) // Go: …e-308 .NET default: …E-308
fmt.Println(999999.0) // Go: 999999 .NET default: 999999 (agree)
The 6 is the whole story for %v: it is why 1000000.0 prints as 1e+06 while 999999.0 prints in full,
and it comes from strconv’s formatDigits, which pins the exponent threshold to a flat 6 whenever the
digits were the shortest round-trip (if shortest { eprec = 6 }) rather than to the requested precision.
The threshold is not 21 — that figure appears in an older reading of the rule and matches JavaScript’s
Number.toString, not Go’s.
The conversion: fmt and strconv are converted from Go’s own source (src/core/fmt,
src/core/strconv, src/core/internal/strconv), so a converted program formats a float through Go’s
FormatFloat itself — its shortest-digit search, formatDigits’ threshold, and the fmtE/fmtF
layout — and .NET’s presentation never applies. Verb defaults, float32 digits (float32(1.0/3.0)
prints 0.33333334), %g’s promotion of a zero precision (%.0g of 0.7 is 0.7), and the ±Inf
and NaN flag handling all come from the same Go code. (Guarded by the FloatFormatExponent
behavioral test — both thresholds from either side, 1e20/1e21, 1000000.0, the
math.MaxFloat64/SmallestNonzeroFloat64 values, float32 counterparts, negative zero, and every verb
with and without precision, byte-compared against go run; ±Inf/NaN flag interactions live in
PrintfWidthFlags.)
A folded constant of a NAMED type carries its type in the fold
overflowingConstLiteral materializes a compile-time integer constant whose value falls outside the C#
int32 range, because C# would otherwise evaluate the operator expression in int32 and overflow
(CS0220). When the constant has a defined type, the fold carries that type in the same parenthesized
(T)(…) shape the native-int arm uses:
d := 8 * time.Hour
secondsEastOfUTC := int((8 * time.Hour).Seconds())
var d = (time.Duration)(28800000000000L);
nint secondsEastOfUTC = (nint)((time.Duration)(28800000000000L)).Seconds();
wholeExprIsCastOfType recognizes that shape, so enclosing paths do not re-wrap it. The generated wrapper
converts implicitly from its underlying, so the cast is always legal, and Go’s own parentheses around a
method-call receiver keep the postfix .M() binding to the cast rather than to the literal.
The type matters twice. A fold read through Underlying() would be a bare basic literal: d would be a
C# long, which prints as its digits where a Duration prints 8h0m0s, and (28800000000000L).Seconds()
does not compile (CS1929, long has no Seconds). Only constants outside int32 reach this arm at all,
so it touches only the few computed time.Duration-class constants above that magnitude. (Guarded by the
PackageNameShadowing behavioral test, case 4.)
← Named Numeric Types and Constant Contexts · Index · Nil and Zero Values →