README
ΒΆ
Money
A Go implementation of Martin Fowler's Money pattern, inspired by moneyphp/money.
π Table of Contents
- Why Use a Money Library?
- Installation
- Requirements
- Features
- Packages
- Quick Start
- Core Concepts
- Supported Currencies
- Error Handling
- Best Practices
- Development
- Differences from MoneyPHP
- License
- Credits
- Contributing
- Resources
π€ Why Use a Money Library?
You shouldn't represent monetary values with floats due to precision issues. This library uses integers internally to avoid floating-point arithmetic errors and provides safe money calculations.
// β DON'T DO THIS
price := 0.1 + 0.2 // 0.30000000000000004
// β
DO THIS
mm := money.NewManager()
a := mm.Create(10, currency.USD) // 10 cents
b := mm.Create(20, currency.USD) // 20 cents
sum, _ := mm.Add(a, b) // $0.30
π Installation
go get github.com/gocanto/money
π Requirements
- Go
1.25.5(pergo.mod)
β¨ Features
- Safe Money Arithmetic: Add, subtract, multiply without floats
- Currency Support: ISO 4217 dataset + custom currencies
- Formatting: Currency-aware formatting and major-unit conversion
- Parsing: Parse human-entered amounts (symbols/codes, separators)
- Aggregation: Sum/Min/Max/Avg via an
Aggregator - Currency Exchange: Convert via an
exchange.Exchange - JSON + DB Integration:
json.Marshaler/Unmarshaler,sql.Scanner/driver.Valuer
π¦ Packages
github.com/gocanto/money/money:Money,Manager,Aggregator,Convertergithub.com/gocanto/money/currency: ISO currency data +Managergithub.com/gocanto/money/exchange: in-memory exchange rates + conversiongithub.com/gocanto/money/parser: parse strings like$1,234.56,EUR 10,50github.com/gocanto/money/format: currency formatter used bycurrency.Currencygithub.com/gocanto/money/exception: sentinel errors used across the module
π Quick Start
package main
import (
"fmt"
"github.com/gocanto/money/currency"
"github.com/gocanto/money/money"
)
func main() {
mm := money.NewManager()
// Amounts are in minor units (cents, pence, etc.)
price := mm.Create(10000, currency.USD) // $100.00
tax := mm.Create(850, currency.USD) // $8.50
total, _ := mm.Add(price, tax)
display, _ := total.Display()
fmt.Println(display) // $108.50
// Convenience constructors (use the default Manager)
eur := money.FromEUR(50000) // β¬500.00
eurDisplay, _ := eur.Display()
fmt.Println(eurDisplay)
}
π§ Core Concepts
Creating Money
mm := money.NewManager()
// Create with amount in minor units (cents, pence, etc.)
usd := mm.Create(10000, currency.USD) // $100.00
// From float (use only for user input conversion; floats are imprecise)
approx := mm.CreateFromFloat(99.99, currency.USD)
// From exact decimal string (recommended for external/user-provided decimals)
exact, _ := mm.CreateFromString("99.99", currency.USD)
// Convenience constructors (use the default Manager)
eur := money.FromEUR(5000) // β¬50.00
jpy := money.FromJPY(10000) // Β₯10000
Arithmetic Operations
Use money.Manager for arithmetic operations.
mm := money.NewManager()
a := mm.Create(10000, currency.USD) // $100.00
b := mm.Create(2500, currency.USD) // $25.00
// Addition
sum, _ := mm.Add(a, b)
sumDisplay, _ := sum.Display()
fmt.Println(sumDisplay) // $125.00
// Subtraction
diff, _ := mm.Subtract(a, b)
diffDisplay, _ := diff.Display()
fmt.Println(diffDisplay) // $75.00
// Multiplication
product, _ := mm.Multiply(a, 2)
productDisplay, _ := product.Display()
fmt.Println(productDisplay) // $200.00
Comparison Operations
mm := money.NewManager()
a := mm.Create(10000, currency.USD)
b := mm.Create(5000, currency.USD)
equals, _ := a.Equals(b) // false
greaterThan, _ := a.GreaterThan(b) // true
lessThan, _ := a.LessThan(b) // false
greaterOrEqual, _ := a.GreaterThanOrEqual(b) // true
lessOrEqual, _ := a.LessThanOrEqual(b) // false
// Compare returns -1, 0, or 1
cmp, _ := a.Compare(b) // 1 (a > b)
// Check properties
a.IsZero() // (false, nil)
a.IsPositive() // (true, nil)
a.IsNegative() // (false, nil)
Money Allocation
Split money without losing pennies due to rounding.
mm := money.NewManager()
m := mm.Create(10000, currency.USD) // $100.00
// Split evenly
parts, _ := mm.Split(m, 3)
// parts[0]: $33.34
// parts[1]: $33.33
// parts[2]: $33.33
// Allocate by ratios
m2 := mm.Create(10000, currency.USD) // $100.00
parts, _ = mm.Allocate(m2, 50, 30, 20)
// parts[0]: $50.00 (50%)
// parts[1]: $30.00 (30%)
// parts[2]: $20.00 (20%)
Formatting and Display
mm := money.NewManager()
m := mm.Create(123456, currency.USD)
// Display formatted
display, _ := m.Display() // "$1,234.56"
// Get as major units (float)
major, _ := m.AsMajorUnits() // 1234.56
// Access raw values
amount, _ := m.Amount() // 123456 (int64)
curr, _ := m.Currency() // *currency.Currency{Code: "USD", ...}
Parsing
import (
"github.com/gocanto/money/money"
"github.com/gocanto/money/parser"
)
p := parser.NewParser()
// Parse with currency symbol
val, currency, err := p.ParseAmount("$100.50") // 100.50, "USD"
m := money.NewManager().CreateFromFloat(val, currency)
val, currency, err = p.ParseAmount("β¬250.75") // 250.75, "EUR"
val, currency, err = p.ParseAmount("Β£99.99") // 99.99, "GBP"
// Parse with currency code
val, currency, err = p.ParseAmount("100.50 USD") // 100.50, "USD"
val, currency, err = p.ParseAmount("EUR 250.75") // 250.75, "EUR"
// Parse with thousands separator
val, currency, err = p.ParseAmount("$1,234.56") // 1234.56, "USD"
// Parse with default currency
val, currency, err = p.ParseAmount("100.50", "USD") // 100.50, "USD"
// Parse just decimal values (no currency)
amount, err := p.ParseDecimal("1234.56") // 1234.56
Decimal Separator Handling
The parser intelligently handles different decimal separator formats:
// Mixed separators - unambiguous
p.ParseAmount("$1,234.56") // US format: 1234.56 USD
p.ParseAmount("β¬1.234,56") // European format: 1234.56 EUR
// Comma-only input - ambiguous
p.ParseAmount("$1,000") // Treated as 1000.00 USD (thousands separator)
p.ParseAmount("β¬10,50") // Treated as 1050.00 EUR (NOT β¬10.50)
// For European decimal-only format, use ParseAmountWithDecimalComma
p.ParseAmountWithDecimalComma("β¬10,50") // Correctly parses as 10.50 EUR
p.ParseDecimalWithComma("10,50") // Correctly parses as 10.50
Important: When only commas are present (no dots), the parser treats them as thousands separators by default. To parse European-style decimal commas (e.g., "10,50" as 10.50), use ParseAmountWithDecimalComma() or ParseDecimalWithComma() methods.
Aggregation
mm := money.NewManager()
agg := money.NewAggregator(mm)
prices := []*money.Money{
mm.Create(10000, currency.USD),
mm.Create(20000, currency.USD),
mm.Create(30000, currency.USD),
}
// Sum all
total, _ := agg.Sum(prices...)
// Get minimum
min, _ := agg.Min(prices...)
// Get maximum
max, _ := agg.Max(prices...)
// Calculate average
avg, _ := agg.Avg(prices...)
Currency Exchange
import (
"github.com/gocanto/money/currency"
"github.com/gocanto/money/exchange"
"github.com/gocanto/money/money"
)
currencies := currency.NewManager()
ex := exchange.NewExchange()
_ = ex.AddRate(currency.USD, currency.EUR, 0.85)
_ = ex.AddRate(currency.USD, currency.GBP, 0.73)
// Get exchange rate
rate, _ := ex.GetRate(currency.USD, currency.EUR) // 0.85
converter, _ := money.NewConverter(currencies, ex)
mm := money.NewManager()
usd := mm.Create(10000, currency.USD) // $100.00
eur, _ := converter.Convert(usd, currency.EUR)
eurDisplay, _ := eur.Display()
fmt.Println(eurDisplay) // β¬85.00
gbp, _ := converter.ConvertWithRate(usd, currency.GBP, 0.73)
gbpDisplay, _ := gbp.Display()
fmt.Println(gbpDisplay) // Β£73.00
JSON Serialization
type Product struct {
Name string `json:"name"`
Price *money.Money `json:"price"`
}
// Marshal
product := Product{
Name: "Book",
Price: money.NewManager().Create(2999, currency.USD),
}
json, _ := json.Marshal(product)
// {"name":"Book","price":{"amount":2999,"currency":"USD"}}
// Unmarshal
var p Product
json.Unmarshal([]byte(jsonStr), &p)
Database Support
type Product struct {
ID int `db:"id"`
Price *money.Money `db:"price"`
}
// Money implements sql.Scanner and driver.Valuer
// Stores as a delimited string: "amount|currency_code" (separator is configurable)
db.Exec("INSERT INTO products (price) VALUES (?)", product.Price)
// Stores as: 2999|USD
Currency Management
import (
"github.com/gocanto/money/currency"
"github.com/gocanto/money/money"
)
// Get currency information
cm := currency.NewManager()
c := cm.FindByCode(currency.USD)
c.Code // "USD"
c.Fraction // 2 (decimal places)
c.Grapheme // "$"
// Get by numeric code (ISO 4217)
c2 := cm.FindByNumericCode("840") // USD
// Add custom currency
cm.AddFrom("BTC", "βΏ", "$1", ".", ",", "000", 8)
mm := money.NewManager()
bitcoin := mm.Create(100000000, "BTC") // 1.00000000 βΏ
π Supported Currencies
The default currency.Manager includes an ISO 4217 dataset (including many historical entries) and can be extended with custom currencies.
The money package also includes many FromXXX(amount int64) helpers (e.g. money.FromUSD) that create Money values using the default manager.
π§ Error Handling
Operations that could fail return errors:
import (
"fmt"
"github.com/gocanto/money/currency"
"github.com/gocanto/money/exception"
"github.com/gocanto/money/money"
)
mm := money.NewManager()
usd := mm.Create(100, currency.USD)
eur := mm.Create(100, currency.EUR)
// This will return error
_, err := mm.Add(usd, eur)
if err != nil {
fmt.Println("Cannot add different currencies:", err)
// Error message: "currencies don't match"
}
Common sentinel errors live in github.com/gocanto/money/exception (e.g. exception.ErrCurrencyMismatch).
β Best Practices
-
Always use minor units: Store amounts as integers in the smallest currency unit (cents, pence, etc.)
// β Good price := money.NewManager().Create(9999, currency.USD) // $99.99 -
Check errors: Money operations can fail (currency mismatch, etc.)
mm := money.NewManager() result, err := mm.Add(a, b) if err != nil { // Handle error } -
Use allocation for splits: Don't manually divide - use Split() or Allocate()
// β Good mm := money.NewManager() m := mm.Create(10000, currency.USD) parts, _ := mm.Split(m, 3) // β Loses pennies third := mm.Create(10000/3, currency.USD) -
Prefer exact decimal strings for user input:
CreateFromString("12.34", "USD")avoids float rounding surprises.
π¨βπ» Development
- Run tests + auto-format:
make tests - Run a per-package test summary:
make pretty-test - Format only:
make format
βοΈ Differences from MoneyPHP
- Uses
int64instead of strings for amounts (supports up to ~92 quadrillion) - Go-idiomatic API (e.g.,
money.FromUSD()helper, andmoney.Managerfor operations) - Database support via
sql.Scanneranddriver.Valuer - Aggregation via
money.Aggregator - Simple in-memory exchange system with explicit rate management
π License
This project is licensed under the MIT License - see the LICENSE file for details.
π Credits
Inspired by moneyphp/money and Martin Fowler's Money pattern.
π€ Contributing
Contributions are welcome! Please feel free to submit a Pull Request.