Open source project: go-money

check out my other open source projects here
go-money
A simple, idiomatic Go library for working with monetary values, implementing Martin Fowler’s Money pattern.
Amounts are stored as int64 minor units (e.g. cents) to avoid floating-point precision issues. All operations return new values — no mutation, no pointers, no panics.
Fowler’s Money pattern compliance
This library implements Martin Fowler’s Money pattern from Patterns of Enterprise Application Architecture:
| Requirement | Implementation |
|---|---|
| Store amount as integer minor units | int64 (cents, pence, etc.) |
| Pair amount with currency | Money struct holds both |
| Currency-aware arithmetic | Add, Subtract return error on mismatch |
| Multiplication | Multiply(int) |
| Allocation without loss | Allocate and Split distribute remainders as single units |
| Value Object equality | Equals(Money) (bool, error) |
| Comparison | GreaterThan, LessThan return (bool, error) |
| Value semantics | Money is a plain struct — no pointers, all operations return new values |
Installation
go get github.com/tiagomelo/go-money
Usage
Creating a monetary value
Amounts are in minor units (cents, pence, etc.).
price := money.NewMoney(1999, money.USD) // $19.99
vat := money.NewMoney(400, money.EUR) // €4.00
Arithmetic
a := money.NewMoney(1000, money.USD)
b := money.NewMoney(250, money.USD)
sum, err := a.Add(b) // $12.50
diff, err := a.Subtract(b) // $7.50
doubled := a.Multiply(2) // $20.00
Operations on different currencies return an error:
usd := money.NewMoney(100, money.USD)
eur := money.NewMoney(100, money.EUR)
_, err := usd.Add(eur) // err: "cannot add EUR to USD"
Comparison
a := money.NewMoney(1000, money.USD)
b := money.NewMoney(500, money.USD)
eq, err := a.Equals(b) // false, nil
gt, err := a.GreaterThan(b) // true, nil
lt, err := a.LessThan(b) // false, nil
same := a.SameCurrency(b) // true
Comparing different currencies returns an error:
usd := money.NewMoney(100, money.USD)
eur := money.NewMoney(100, money.EUR)
_, err := usd.GreaterThan(eur) // err: "currency mismatch: USD and EUR"
Absolute value
m := money.NewMoney(-500, money.USD)
abs := m.Absolute() // $5.00
Splitting
Divides an amount into n equal parts. Any remainder is distributed one unit at a time to the first parts, ensuring no value is lost.
m := money.NewMoney(101, money.USD) // $1.01
parts, err := m.Split(4)
// parts[0] = $0.26 (gets the leftover cent)
// parts[1] = $0.25
// parts[2] = $0.25
// parts[3] = $0.25
// sum = $1.01 ✓
Allocation
Distributes an amount according to ratios. Remainder is distributed one unit at a time to the first allocations, ensuring no value is lost.
m := money.NewMoney(100, money.USD) // $1.00
// 2:1 split
parts, err := m.Allocate(2, 1)
// parts[0] = $0.67
// parts[1] = $0.33
// sum = $1.00 ✓
// Equal three-way split with a remainder
m2 := money.NewMoney(101, money.USD) // $1.01
parts, err = m2.Allocate(1, 1, 1)
// parts[0] = $0.34 (gets the leftover cent)
// parts[1] = $0.34
// parts[2] = $0.33
// sum = $1.01 ✓
Display
m := money.NewMoney(123456, money.USD)
fmt.Println(m.Display()) // $1,234.56
fmt.Println(m.AsMajorUnits()) // 1234.56 (float64, display only)
AsMajorUnits returns a float64 and should never be used for arithmetic.
JSON
Money implements json.Marshaler and json.Unmarshaler. The amount is serialized in minor units.
m := money.NewMoney(1999, money.USD)
b, err := json.Marshal(m)
// {"amount":1999,"currency":"USD"}
var got money.Money
err = json.Unmarshal(b, &got)
// got == m
// Unknown currency codes are rejected:
err = json.Unmarshal([]byte(`{"amount":100,"currency":"XYZ"}`), &got)
// err: "unknown currency code: XYZ"
Database storage
Money and Currency both implement driver.Valuer and sql.Scanner, so they work directly with database/sql and any compatible ORM.
The recommended schema stores amount and currency in two separate columns:
CREATE TABLE products (
id SERIAL PRIMARY KEY,
price_amount BIGINT NOT NULL, -- minor units (e.g. cents)
price_currency VARCHAR(3) NOT NULL -- ISO 4217 code (e.g. 'USD')
);
Writing:
m := money.NewMoney(1999, money.USD) // $19.99
amountVal, _ := m.Value() // int64(1999)
currencyVal, _ := m.Currency.Value() // "USD"
db.Exec(
`INSERT INTO products (price_amount, price_currency) VALUES ($1, $2)`,
amountVal, currencyVal,
)
Reading:
var m money.Money
row := db.QueryRow(`SELECT price_amount, price_currency FROM products WHERE id = $1`, id)
err := row.Scan(&m.Amount, &m.Currency)
// m is fully reconstructed, including all currency formatting metadata
Currency.Scan validates the code and returns an error for unknown values:
var c money.Currency
err := c.Scan("XYZ") // err: "unknown currency code: XYZ"
Supported currencies
All ISO 4217 currencies are supported as package-level variables:
money.USD money.EUR money.GBP money.JPY
money.BRL money.CAD money.AUD money.CHF
// ... and 150+ more
Design notes
- Minor units: amounts are always
int64minor units.NewMoney(100, money.USD)is $1.00, not $100. - Value semantics:
Moneyis a plain struct, not a pointer. Assigning it copies it. - No panics: all error conditions (currency mismatch, invalid split/allocate arguments, unknown currency) return
error. - Type-safe currencies: currency codes are a dedicated unexported type; only the predefined package variables (
money.USD,money.EUR, etc.) are valid — you cannot pass an arbitrary string. - Remainder-safe arithmetic:
SplitandAllocateguarantee thatsum(parts) == original, distributing any remainder as single units to the first elements. - Database-ready:
MoneyandCurrencyimplementdriver.Valuerandsql.Scanner. Store them as two columns — amount (BIGINT) and currency code (VARCHAR(3)). Unknown currency codes are rejected on scan.
Running tests
make test
With coverage report:
make coverage
License
MIT.