How to represent currency in Go?

Learn how to represent currency in go? with practical examples, diagrams, and best practices. Covers go, currency development techniques with visual explanations.

Representing Currency in Go: Best Practices and Pitfalls

Hero image for How to represent currency in Go?

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

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

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.
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