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

Jay Kye
8 min read

Exploring one of programming's most infamous quirks: floating-point arithmetic errors. Learn why 0.1 + 0.2 doesn't equal 0.3 in most programming languages, and how to build a precise arithmetic calculator in Go using the decimal package.

GoGolangDecimalFloating PointComputer SciencePrecisionBinaryCalculator

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:

  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

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:

  • 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")
}

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:

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