— notes
Floating-Point Tolerance Testing in Go
Last Tuesday, I was knee-deep in a financial calculation service when my tests started failing. You know that sinking feeling when 0.1 + 0.2 doesn’t equal 0.3? That was my afternoon.
This got me thinking about when we actually need tolerance-based comparisons versus when we’re just cargo-culting best practices.
graph LR
A["0.1 + 0.2"] --> B["IEEE 754"]
B --> C["0.30000...04"]
D["0.3"] --> E["IEEE 754"]
E --> F["0.29999...99"]
C --> G["Gap = epsilon"]
F --> GWhen tolerance matters
I was working on a discount calculation system:
func calculateDiscount(price, rate float64) float64 {
return price * rate
}
func TestDiscountCalculation(t *testing.T) {
price := 29.99
rate := 0.15
expected := 4.4985
result := calculateDiscount(price, rate)
if result != expected {
t.Errorf("got %v, want %v", result, expected)
}
}With tolerance, you’d handle it like this:
func almostEqual(a, b, tolerance float64) bool {
return math.Abs(a-b) <= tolerance
}
func TestDiscountCalculationWithTolerance(t *testing.T) {
price := 29.99
rate := 0.15
expected := 4.4985
result := calculateDiscount(price, rate)
if !almostEqual(result, expected, 1e-9) {
t.Errorf("got %v, want %v", result, expected)
}
}When you DON’T need tolerance
Some comparisons are fine without tolerance:
if value == 0.0 { } // Zero has exact representation
if math.IsNaN(result) { } // Special values
if result == math.Inf(1) { } // Infinity comparisonsIf your floats haven’t been through different computational paths, exact comparison often works fine.
flowchart TD
A["Comparing floats?"] --> B{"Result of\narithmetic?"}
B -- Yes --> C["Use epsilon"]
B -- No --> D{"Integer-\nconvertible?"}
D -- Yes --> E["No epsilon needed"]
D -- No --> F{"Currency?"}
F -- Yes --> G["Use integer cents"]
F -- No --> CA practical helper
Here’s a utility I use across projects:
const DefaultFloatTolerance = 1e-9
func FloatEquals(a, b float64) bool {
return FloatEqualsWithTolerance(a, b, DefaultFloatTolerance)
}
func FloatEqualsWithTolerance(a, b, tolerance float64) bool {
if math.IsNaN(a) && math.IsNaN(b) {
return true
}
if math.IsInf(a, 0) && math.IsInf(b, 0) {
return math.Signbit(a) == math.Signbit(b)
}
return math.Abs(a-b) <= tolerance
}Use tolerance when your floats have been through computational journeys. Skip it for direct assignments, constants, and same-path calculations.