If you've spent any time programming, you've likely encountered this mind-bending phenomenon:
0.1 + 0.2 === 0.3 // false (!?)
0.1 + 0.2 // 0.30000000000000004
Wait... what? How can basic arithmetic be wrong? This isn't a bug—it's a fundamental limitation of how computers represent decimal numbers. After stumbling upon this issue in a financial calculation project, I decided to dive deep and build a solution: a precise arithmetic calculator in Go using the decimal package.
Project Repository: go-decimal on GitHub
The Problem: Floating-Point Representation
Why Computers Struggle with 0.1
Computers operate in binary (base-2), not decimal (base-10). While integers have exact binary representations, many decimal fractions do not translate cleanly to binary.
Analogy: Just as 1/3 in decimal is a repeating decimal (0.333...), 0.1 in binary is a repeating binary fraction:
0.1 (decimal) = 0.0001100110011001100110011... (binary, repeating)
Since computers have finite memory, they can't store infinite repeating fractions. Instead, they store the closest possible approximation within the allocated bits (typically 32 or 64 bits for floating-point numbers).
The Accumulation of Tiny Errors
When you perform arithmetic operations like 0.1 + 0.2, the computer is actually adding two slightly inaccurate binary approximations:
0.1 (stored) ≈ 0.1000000000000000055511151231257827...
+ 0.2 (stored) ≈ 0.2000000000000000111022302462515654...
= 0.3000000000000000444089209850062616... ≠ 0.3
These tiny inaccuracies compound, leading to results that are very close to, but not exactly equal to, the expected value.
Real-World Implications
1. Financial Calculations:
Imagine calculating interest on a bank account:
balance := 100.10
interest := 0.20
newBalance := balance + interest // 100.30000000000001 (?!)
In financial systems, even a fraction of a cent matters. Errors accumulate over millions of transactions.
2. Equality Comparisons:
a := 0.1 + 0.2
b := 0.3
if a == b { // This will be false!
fmt.Println("Equal")
}
Direct equality comparison of floats is unreliable. Instead, use epsilon tolerance:
epsilon := 0.00001
if math.Abs(a - b) < epsilon {
fmt.Println("Approximately equal")
}
3. Scientific Computing:
Long-running simulations can accumulate errors, leading to significant deviations from expected results.
The Solution: Go's Decimal Package
To tackle this problem, I built a simple but precise arithmetic calculator using the shopspring/decimal package for Go. This package provides arbitrary-precision fixed-point decimal numbers, eliminating floating-point errors.
Project Overview
What it does:
- Accepts two decimal numbers from user input
- Performs basic arithmetic operations (+, -, *, /)
- Returns exactly precise results (no floating-point errors)
Key Features:
- Handles any decimal numbers (not limited to float64)
- Uses arbitrary-precision arithmetic
- Simple CLI interface
How It Works
Step 1: Read User Input
reader := bufio.NewReader(os.Stdin)
fmt.Print("Enter the first number: ")
input1, _ := reader.ReadString('\n')
input1 = strings.TrimSpace(input1)
Create a reader for standard input and read until a newline character is encountered.
Step 2: Convert String to Decimal
num1, err := decimal.NewFromString(input1)
if err != nil {
fmt.Println("Invalid number:", err)
return
}
The decimal.NewFromString() method parses the user's string input and creates a precise decimal representation. This is the magic that avoids floating-point approximations.
Step 3: Get Second Number
fmt.Print("Enter the second number: ")
input2, _ := reader.ReadString('\n')
input2 = strings.TrimSpace(input2)
num2, err := decimal.NewFromString(input2)
if err != nil {
fmt.Println("Invalid number:", err)
return
}
Repeat the process for the second number.
Step 4: Perform Operation
fmt.Print("Enter the operation (+, -, *, /): ")
operation, _ := reader.ReadString('\n')
operation = strings.TrimSpace(operation)
var result decimal.Decimal
switch operation {
case "+":
result = num1.Add(num2)
case "-":
result = num1.Sub(num2)
case "*":
result = num1.Mul(num2)
case "/":
if num2.IsZero() {
fmt.Println("Error: Division by zero")
return
}
result = num1.Div(num2)
default:
fmt.Println("Invalid operation")
return
}
fmt.Println("RESULT:", result)
Use the built-in arithmetic methods (Add, Sub, Mul, Div) provided by the decimal type. These methods perform exact decimal arithmetic.
Example Usage
$ go run main.go
Enter the first number: 0.1
Enter the second number: 0.2
Enter the operation (+, -, *, /): +
RESULT: 0.3 ✅ Exactly 0.3, not 0.30000000000000004!
More Examples:
# Multiplication with precision
Enter the first number: 0.1
Enter the second number: 0.3
Enter the operation (+, -, *, /): *
RESULT: 0.03
# Division with precision
Enter the first number: 1
Enter the second number: 3
Enter the operation (+, -, *, /): /
RESULT: 0.3333333333333333333333333333 # Configurable precision
How to Run the Project
Installation:
git clone https://github.com/jayk0001/go-decimal.git
cd go-decimal
Run:
go run main.go
The program will prompt you for:
- First number
- Second number
- Operation (+, -, *, /)
Then display the precise result.
Under the Hood: How Decimal Works
The decimal package represents numbers as a combination of:
- Coefficient (integer): The significant digits
- Exponent (integer): The power of 10
For example:
0.1 = 1 × 10^(-1)
coefficient = 1
exponent = -1
This representation avoids binary fractions entirely, storing numbers in a way that naturally aligns with decimal arithmetic—just like we do by hand.
Advantages of Decimal Package
1. Exact Precision:
d1 := decimal.NewFromFloat(0.1)
d2 := decimal.NewFromFloat(0.2)
result := d1.Add(d2)
// result.String() == "0.3" ✅
2. Configurable Precision:
result := decimal.NewFromInt(1).Div(decimal.NewFromInt(3))
fmt.Println(result.StringFixed(2)) // "0.33"
fmt.Println(result.StringFixed(10)) // "0.3333333333"
3. Financial Calculations:
price := decimal.NewFromString("19.99")
taxRate := decimal.NewFromString("0.08")
tax := price.Mul(taxRate).Round(2) // Rounds to 2 decimal places
total := price.Add(tax)
4. Safe Comparisons:
a := decimal.NewFromFloat(0.1).Add(decimal.NewFromFloat(0.2))
b := decimal.NewFromFloat(0.3)
if a.Equal(b) { // This works correctly!
fmt.Println("Equal")
}
Trade-offs
Performance:
-
decimalis slower than nativefloat64operations - For most applications (especially financial), accuracy > speed
Memory:
- Uses more memory than fixed-size floats
- Acceptable trade-off for precision-critical applications
When to Use:
- ✅ Financial calculations (money, interest, taxes)
- ✅ Precise decimal arithmetic requirements
- ✅ Comparisons requiring exactness
- ❌ Heavy scientific computing (where approximation is acceptable)
- ❌ Graphics/game programming (speed is critical)
Key Learnings
1. Floating-Point Arithmetic is Approximate
Floats are designed for speed and range, not precision. Understanding this limitation prevents bugs in critical applications.
2. Choose the Right Tool
- float64: General-purpose calculations, scientific computing
- decimal: Financial, accounting, precise decimal arithmetic
- big.Float: Arbitrary precision with floating-point semantics
- big.Rat: Exact rational number arithmetic
3. IEEE 754 Standard
The floating-point behavior is standardized (IEEE 754). This means:
- The "bug" exists in all languages (JavaScript, Python, Java, C++, Go, etc.)
- It's not a flaw—it's a trade-off for efficiency
- Understanding it makes you a better programmer
4. Testing Floating-Point Code
Never use == for float comparisons in tests:
// ❌ Bad
if result == 0.3 {
t.Error("Test failed")
}
// ✅ Good (for floats)
epsilon := 0.00001
if math.Abs(result - 0.3) > epsilon {
t.Error("Test failed")
}
// ✅ Best (for decimal)
expected := decimal.NewFromFloat(0.3)
if !result.Equal(expected) {
t.Error("Test failed")
}
Practical Applications
1. E-commerce Pricing
type Product struct {
Name string
Price decimal.Decimal
}
func CalculateTotal(items []Product, taxRate decimal.Decimal) decimal.Decimal {
subtotal := decimal.Zero
for _, item := range items {
subtotal = subtotal.Add(item.Price)
}
tax := subtotal.Mul(taxRate).Round(2)
return subtotal.Add(tax)
}
2. Currency Conversion
func ConvertCurrency(amount decimal.Decimal, exchangeRate decimal.Decimal) decimal.Decimal {
return amount.Mul(exchangeRate).Round(2)
}
// Example
usd := decimal.NewFromString("100.00")
rate := decimal.NewFromString("1.18") // USD to EUR
eur := ConvertCurrency(usd, rate)
fmt.Println(eur) // Exactly 118.00
3. Interest Calculations
func CalculateCompoundInterest(principal, rate decimal.Decimal, years int) decimal.Decimal {
one := decimal.NewFromInt(1)
multiplier := one.Add(rate)
result := principal
for i := 0; i < years; i++ {
result = result.Mul(multiplier)
}
return result.Round(2)
}
Beyond Go: Solutions in Other Languages
Python:
from decimal import Decimal
a = Decimal('0.1')
b = Decimal('0.2')
result = a + b # Decimal('0.3')
JavaScript:
// Use libraries like decimal.js or big.js
const Decimal = require('decimal.js');
const a = new Decimal(0.1);
const b = new Decimal(0.2);
const result = a.plus(b); // 0.3
Java:
import java.math.BigDecimal;
BigDecimal a = new BigDecimal("0.1");
BigDecimal b = new BigDecimal("0.2");
BigDecimal result = a.add(b); // 0.3
Conclusion
The 0.1 + 0.2 != 0.3 phenomenon isn't a programming bug—it's a fundamental limitation of binary floating-point representation. While this quirk can cause headaches, understanding why it happens and knowing the tools to address it makes you a more effective engineer.
Building this simple calculator taught me:
- The importance of choosing the right data type for the job
- How computers represent numbers at a fundamental level
- Why financial applications require special handling
- The value of hands-on experimentation in understanding CS concepts
For applications where precision matters (finance, accounting, scientific measurements), always reach for decimal arithmetic libraries. Your users—and your QA team—will thank you.
Resources:
- Project Repository: go-decimal
- Shopspring Decimal Package
- IEEE 754 Floating Point Standard
- What Every Programmer Should Know About Floating-Point Arithmetic
Have you encountered floating-point precision issues in your projects? How did you solve them? Let's discuss!
Top comments (0)