How to represent currency in Go?
Categories:
Representing Currency in Go: Best Practices and Pitfalls

Learn how to accurately and safely handle monetary values in Go, avoiding common floating-point inaccuracies and ensuring precision in financial applications.
Representing currency in any programming language, especially Go, requires careful consideration to avoid common pitfalls like floating-point inaccuracies. Directly using float32
or float64
for monetary values can lead to subtle bugs and incorrect calculations, which are unacceptable in financial applications. This article explores robust strategies for handling currency in Go, focusing on precision, safety, and best practices.
Why Floating-Point Numbers Are Dangerous for Currency
Floating-point numbers (like float32
and float64
in Go) are designed to approximate real numbers over a wide range, not to represent exact decimal values. This approximation can lead to unexpected results when performing arithmetic operations, as many decimal fractions cannot be precisely represented in binary. For example, 0.1 + 0.2
might not exactly equal 0.3
.
package main
import (
"fmt"
)
func main() {
var a float64 = 0.1
var b float64 = 0.2
var c float64 = 0.3
fmt.Printf("0.1 + 0.2 = %.17f\n", a+b) // Output: 0.1 + 0.2 = 0.30000000000000004
fmt.Printf("0.3 = %.17f\n", c) // Output: 0.3 = 0.29999999999999999
fmt.Println("0.1 + 0.2 == 0.3?", a+b == c) // Output: 0.1 + 0.2 == 0.3? false
}
Demonstration of floating-point inaccuracy in Go
float32
or float64
directly for storing or calculating monetary values. The slight inaccuracies can accumulate and lead to significant errors in financial systems.Recommended Approaches for Currency Representation
There are two primary robust approaches for representing currency in Go: using integers to store the smallest currency unit or using a dedicated decimal type library.
1. Using Integers (Smallest Unit)
This is a widely adopted and highly recommended method. Instead of storing 12.34
dollars, you store 1234
cents. All calculations are performed using integers, eliminating floating-point errors. When displaying the value, you convert it back to a decimal representation by dividing by 100 (or the appropriate power of 10 for the currency's smallest unit).
This approach is simple, efficient, and guarantees precision. It's crucial to consistently apply this method throughout your application.
package main
import (
"fmt"
)
// Money represents a monetary value in cents (or the smallest currency unit).
// Using int64 to avoid overflow for large sums.
type Money int64
// NewMoney creates a Money type from dollars and cents.
func NewMoney(dollars, cents int) Money {
return Money(dollars*100 + cents)
}
// Add adds two Money values.
func (m Money) Add(other Money) Money {
return m + other
}
// Subtract subtracts one Money value from another.
func (m Money) Subtract(other Money) Money {
return m - other
}
// Format returns the monetary value as a string (e.g., "$12.34").
func (m Money) Format() string {
dollars := m / 100
cents := m % 100
return fmt.Sprintf("$%d.%02d", dollars, cents)
}
func main() {
item1 := NewMoney(12, 34) // $12.34
item2 := NewMoney(5, 99) // $5.99
total := item1.Add(item2)
fmt.Println("Item 1:", item1.Format())
fmt.Println("Item 2:", item2.Format())
fmt.Println("Total:", total.Format()) // Output: Total: $18.33
// Example with floating point input (careful conversion needed)
var floatInput float64 = 1.99
// Convert float to integer cents, handling potential float inaccuracies during conversion
floatAsMoney := Money(floatInput * 100.0) // This still has float risk if floatInput isn't exact
fmt.Println("Float input as Money:", floatAsMoney.Format())
// Better conversion from string or explicit dollars/cents
item3 := NewMoney(1, 99)
fmt.Println("Item 3 (from explicit):", item3.Format())
// Multiplication example (e.g., tax calculation)
price := NewMoney(10, 0)
taxRate := 0.05 // 5%
// To multiply, convert to float for calculation, then back to int, or use integer-based multiplication
// Integer-based multiplication for precision:
tax := price * Money(5) / Money(100) // 1000 cents * 5 / 100 = 50 cents
fmt.Println("Price:", price.Format())
fmt.Println("Tax (5%):", tax.Format()) // Output: Tax (5%): $0.50
}
Implementing currency with int64
(smallest unit)
flowchart TD A[User Input: "$12.34"] --> B{Parse String} B --> C[Convert to Cents: 1234] C --> D[Store as int64] D -- Arithmetic Operations --> E[Perform Calculations (int64)] E --> F[Result: 1833] F --> G{Convert to Dollars/Cents} G --> H[Format for Display: "$18.33"]
Workflow for integer-based currency handling
2. Using a Dedicated Decimal Type Library
For more complex financial calculations, or when dealing with currencies that have more than two decimal places (e.g., cryptocurrencies, or specific financial instruments), a dedicated decimal type library can be invaluable. These libraries provide types that represent decimal numbers precisely, avoiding the issues of floating-point arithmetic.
Popular Go libraries include shopspring/decimal
and cockroachdb/apd
.
package main
import (
"fmt"
"github.com/shopspring/decimal"
)
func main() {
// Create decimal values
item1 := decimal.NewFromFloat(12.34)
item2 := decimal.NewFromFloat(5.99)
// Perform addition
total := item1.Add(item2)
fmt.Println("Item 1:", item1)
fmt.Println("Item 2:", item2)
fmt.Println("Total:", total) // Output: Total: 18.33
// Perform multiplication (e.g., tax calculation)
price := decimal.NewFromFloat(10.00)
taxRate := decimal.NewFromFloat(0.05) // 5%
tax := price.Mul(taxRate)
fmt.Println("Price:", price)
fmt.Println("Tax (5%):", tax) // Output: Tax (5%): 0.5
// Division example
amount := decimal.NewFromFloat(100.00)
shares := decimal.NewFromInt(3)
// Divide with specified precision (e.g., 2 decimal places for currency)
perShare := amount.DivRound(shares, 2)
fmt.Println("Amount:", amount)
fmt.Println("Shares:", shares)
fmt.Println("Per Share (rounded to 2 places):", perShare) // Output: Per Share (rounded to 2 places): 33.33
}
Using shopspring/decimal
for precise currency calculations
decimal.Decimal
values. Prefer NewFromString
or NewFromInt
over NewFromFloat
if possible, to avoid introducing floating-point inaccuracies at the very beginning.Choosing the Right Approach
The choice between using integers and a decimal library depends on the complexity of your financial logic and performance requirements:
- Integer-based (smallest unit): Ideal for most standard currency operations (addition, subtraction, simple multiplication/division) where the number of decimal places is fixed and small (e.g., 2 for USD, EUR). It's very performant and avoids external dependencies.
- Decimal library: Recommended for scenarios involving arbitrary precision, complex rounding rules, division that results in many decimal places, or when dealing with currencies that have varying decimal precision. It adds a dependency and might have a slight performance overhead compared to native integer arithmetic, but offers greater flexibility and safety for advanced use cases.
struct
combining the amount and currency code is a good practice.package main
import (
"fmt"
"errors"
"github.com/shopspring/decimal"
)
// CurrencyCode represents a standard currency code (e.g., "USD", "EUR").
type CurrencyCode string
const (
USD CurrencyCode = "USD"
EUR CurrencyCode = "EUR"
JPY CurrencyCode = "JPY"
)
// MonetaryValue combines an amount with its currency code.
type MonetaryValue struct {
Amount decimal.Decimal
Currency CurrencyCode
}
// NewMonetaryValue creates a new MonetaryValue.
func NewMonetaryValue(amount float64, currency CurrencyCode) MonetaryValue {
return MonetaryValue{
Amount: decimal.NewFromFloat(amount),
Currency: currency,
}
}
// Add adds two MonetaryValue instances. Returns an error if currencies don't match.
func (mv MonetaryValue) Add(other MonetaryValue) (MonetaryValue, error) {
if mv.Currency != other.Currency {
return MonetaryValue{}, errors.New("cannot add different currencies")
}
return MonetaryValue{
Amount: mv.Amount.Add(other.Amount),
Currency: mv.Currency,
}, nil
}
func main() {
priceUSD := NewMonetaryValue(19.99, USD)
taxUSD := NewMonetaryValue(1.50, USD)
totalUSD, err := priceUSD.Add(taxUSD)
if err != nil {
fmt.Println("Error:", err)
} else {
fmt.Printf("Total USD: %s %s\n", totalUSD.Amount.StringFixed(2), totalUSD.Currency)
}
priceEUR := NewMonetaryValue(15.00, EUR)
// Attempt to add different currencies
_, err = totalUSD.Add(priceEUR)
if err != nil {
fmt.Println("Error adding different currencies:", err)
} // Output: Error adding different currencies: cannot add different currencies
}
Using a struct to combine amount and currency code for safety