DEV Community

jayk0001
jayk0001

Posted on

Why 0.1 + 0.2 != 0.3: Building a Precise Calculator with Go's Decimal Package

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 (?!)
Enter fullscreen mode Exit fullscreen mode

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")
}
Enter fullscreen mode Exit fullscreen mode

Direct equality comparison of floats is unreliable. Instead, use epsilon tolerance:

epsilon := 0.00001
if math.Abs(a - b) < epsilon {
    fmt.Println("Approximately equal")
}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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!
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

How to Run the Project

Installation:

git clone https://github.com/jayk0001/go-decimal.git
cd go-decimal
Enter fullscreen mode Exit fullscreen mode

Run:

go run main.go
Enter fullscreen mode Exit fullscreen mode

The program will prompt you for:

  1. First number
  2. Second number
  3. 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
Enter fullscreen mode Exit fullscreen mode

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" ✅
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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")
}
Enter fullscreen mode Exit fullscreen mode

Trade-offs

Performance:

  • decimal is slower than native float64 operations
  • 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")
}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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')
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Java:

import java.math.BigDecimal;

BigDecimal a = new BigDecimal("0.1");
BigDecimal b = new BigDecimal("0.2");
BigDecimal result = a.add(b);  // 0.3
Enter fullscreen mode Exit fullscreen mode

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:

Have you encountered floating-point precision issues in your projects? How did you solve them? Let's discuss!

Top comments (0)