A Go implementation of Martin Fowler's Money pattern, inspired by moneyphp/money.
- 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
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.30go get github.com/gocanto/money- Go
1.25.5(pergo.mod)
- 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
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
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)
}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) // ¥10000Use 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.00mm := 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)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%)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", ...}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.56The 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.50Important: 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.
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...)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.00type 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)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|USDimport (
"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 ₿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.
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).
-
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.
- Run tests + auto-format:
make tests - Run a per-package test summary:
make pretty-test - Format only:
make format
- 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
This project is licensed under the MIT License - see the LICENSE file for details.
Inspired by moneyphp/money and Martin Fowler's Money pattern.
Contributions are welcome! Please feel free to submit a Pull Request.