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)