currency keeps money in decimal, not floats or minor units
Currency handling for Go.
At a glance
- What is it?
- bojanz/currency is a MIT licensed Go library for currency amounts, built on an arbitrary-precision decimal type with locale formatting data from CLDR v48. Its Amount type has value semantics, splits money with a defined remainder rule, and plugs into PostgreSQL through the standard driver interfaces, all with a single dependency.
- Who is it for?
- currency suits a Go service that stores prices, applies exchange rates and renders them in a customer's locale, and it is the wrong choice for an application that only ever handles one currency in whole cents, where an integer type is simpler and faster.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 47 days ago.
- What is it written in?
- Mainly Go, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
A money type for Go, with CLDR data behind it
bojanz/currency handles currency amounts and provides currency information and formatting for Go. The package documentation lives on pkg.go.dev, the licence is MIT, and the project is a single Go module with a very small dependency footprint.
The scope is three things. Currency codes with their numeric codes and fraction digits, currency symbols and formats for all locales, and a country to currency mapping. On top of that sits an Amount struct with value semantics, following Fowler's Money pattern, and a Formatter for formatting and parsing amounts. The library also lets you register custom currencies or override existing ones, which is the hook you need if you trade in something that is not an ISO currency.
The data side is the interesting engineering decision. The library is powered by CLDR v48 and carries about 40kb of data, which is a lot smaller than the full CLDR release. The filtering is described in the design goals: the modern subset of locales is used, cutting the list from roughly 560 to roughly 370, and once the data is gathered it is stripped of everything the package does not use and then deduplicated by parent locale, so fr-CH is dropped if fr holds the same data.
The audience is a Go developer who has to store, calculate and display prices. The last push was on 2026-09-06, and the recent releases are v1.5.0 on 2026-09-06, v1.4.4 on 2026-04-30 and v1.4.3 on 2026-04-15.
Why floats and minor units both fail
The design goals section explains the type choice in a way that is worth reading before the API documentation.
Currency amounts cannot be floats. Beyond the usual rounding argument, the README makes a second point: storing integer minor units, so 2.99 becomes 299, becomes problematic as soon as there are multiple currencies, because it is difficult to sort in a database, and whenever sub-minor-unit precision is required, for example because of merchant or tax rules.
That leaves a real arbitrary-precision decimal type, and Go does not have one natively. The library therefore uses a userspace implementation provided by the cockroachdb/apd package. The module file shows that as the single requirement, apd/v3 at version 3.2.1, and the module targets Go 1.24.
The design point is what sits on top. The Amount struct is an easy to use abstraction over the decimal implementation, and the documentation says explicitly that this is done so the underlying implementation can be replaced in the future without a backwards compatibility break. That is a deliberate choice about what is part of the public contract: the arithmetic behaviour of Amount is, the decimal library underneath is not.
It is also worth being clear about what this buys. An amount carries its currency, so an amount is not a bare number that can be accidentally added to a different currency's amount, and the arithmetic operations are defined on the pair.
Split decides where the leftover cent goes
The README's example is short enough to read as a specification:
amount, _ := currency.NewAmount("275.98", "EUR")
total, _ := amount.Mul("4")
installments, _ := total.Split(3) // 367.98, 367.97, 367.97
locale := currency.NewLocale("fr")
formatter := currency.NewFormatter(locale)Three things are visible there. NewAmount parses a decimal string and a currency code, so 275.98 euros becomes an amount. Mul multiplies it, and the multiplier is also given as a string, which keeps the arithmetic in the decimal domain rather than promoting to float. Split then divides 1103.92 into three parts and the comment shows the result, 367.98, 367.97 and 367.97, so the remainder is not lost and is not spread evenly either, it goes into the first installment.
That rule is the kind of detail that matters to whoever has to reconcile an invoice, and it is visible in the example rather than buried in a changelog entry. If your billing rules distribute the remainder differently, this is the behaviour to check.
The rest of the arithmetic surface follows the same shape. There is a Convert method that takes a target currency and a rate, and a Round method, which the example applies after conversion, so a converted amount is rounded to the target currency's fraction digits rather than carrying extra precision into the display. Conversion takes the rate as a string, so the rate is never a float either.
How 40kb of CLDR data is squeezed down
The data filtering is described well enough in the documentation to be worth repeating, because it explains why the bundle is small.
The first step is the locale subset. The modern subset of CLDR locales is used, which the documentation says reduces the list from about 560 to about 370 locales.
The second step is removal. Once the data is gathered, locales are filtered to drop everything the package does not use.
The third step is deduplication by parent. A child locale is not kept if its parent holds the same data, so fr-CH goes when fr covers it. That single rule removes a large amount of repetition from the tables.
The fourth step is symbol grouping. Symbols are grouped together to avoid repeating the same symbol entry, and the documentation shows the shape of the result for the Argentine peso, where the code itself is the symbol for en and fr-CA, the plain dollar sign covers es-AR, and the dollar sign with the AR suffix covers fr.
The fifth step is a deliberate omission. Currency names are not included, because they are rarely shown but take up significant space. The documentation points instead at Intl.DisplayNames in the browser, which is a sensible split for a web application that can resolve the name client side.
The repository layout supports the story. There is a data.go for the generated tables, a gen.go for the generator, and separate files for amount.go, currency.go, formatter.go and locale.go, alongside their tests and a bench_test.go.
Formatting and parsing per locale, and the missing currency names
The formatter is created from a locale and formats an amount into the conventions of that locale. In the example, the French locale is built with currency.NewLocale and the formatter with currency.NewFormatter, and formatting the total produces the French grouping and separator conventions, with the euro sign appended.
The formatter is not only for output. The feature list describes it as a formatter for formatting amounts and parsing formatted amounts, so a string that a user typed into a price field can be read back into an Amount. That is the direction that matters for a checkout form, where the user writes 1.103,92 and the system has to understand it.
The locale object is the unit of formatting, and locales are created by their identifier, so fr and fa in the example. The data behind them comes from CLDR, which is where the symbol placement, the grouping and the decimal separator conventions come from, filtered as described earlier.
What the library deliberately does not carry is the currency name in the server-side data. The consequence for a backend is small, since the name is rarely needed for computation and is usually rendered in a client anyway. The consequence for a developer who needs a name on the server, for a report or an email, is that they have to bring their own data or resolve it in the browser with Intl.DisplayNames.
Storing an amount in a PostgreSQL composite type
The database story is handled through the standard interfaces. Because Amount implements driver.Valuer and sql.Scanner, applications using the pgx driver can store amounts in a composite type, with the number and the currency code stored together in one column.
The example schema in the documentation is short:
CREATE TYPE price AS (
number NUMERIC,
currency_code TEXT
);
CREATE TABLE products (
id CHAR(26) PRIMARY KEY,
name TEXT NOT NULL,
price price NOT NULL,
created_at TIMESTAMPTZ NOT NULL,
updated_at TIMESTAMPTZ
);One detail is called out: the number and currency_code columns can have any name, only their ordering matters. That is the sort of note that saves an hour when someone else picks up the schema.
On the Go side, the struct field is a currency.Amount:
type Product struct {
ID string
Name string
Price currency.Amount
CreatedAt time.Time
UpdatedAt time.Time
}and the scan reads the composite column straight into the struct field:
p := Product{}
row := tx.QueryRow(ctx, `SELECT id, name, price, created_at, updated_at FROM products WHERE id = $1`, id)
err := row.Scan(&p.ID, &p.Name, &p.Price, &p.CreatedAt, &p.UpdatedAt)The repository links database integration notes in its wiki with examples for MySQL, MariaDB and SQLite, so the composite type is the PostgreSQL path rather than the only path.
Value semantics, comparison and the cost of the dependency
Two details make the type pleasant to use in a codebase.
The first is value semantics. The Amount is a struct that behaves like a value, following Fowler's Money pattern, so an amount can be passed around and compared without you having to think about whether two references point at the same money. The second is the built-in Equal method, which the documentation says makes Amount structs comparable via google/go-cmp, and that matters in practice because it is how you assert on money in tests without writing a bespoke comparison.
The cost side is small but real. There is exactly one dependency, the arbitrary-precision decimal implementation, and the module requires Go 1.24. The library also allows custom currencies to be registered or existing ones overridden, which is how you would model a token, a loyalty point or a currency that a payment provider has invented, and it is a documented feature rather than a hack.
On maintenance, the repository is not archived, the last push was on 2026-09-06, and the release tags show a project that ships small versions regularly rather than in bursts. The MIT licence means the only obligation is keeping the copyright notice and licence text with a distributed copy, which is the simplest case for a library you intend to vendor into a service.
Editorial conclusion
currency suits a Go service that stores prices, applies exchange rates and renders them in a customer's locale, and it is the wrong choice for an application that only ever handles one currency in whole cents, where an integer type is simpler and faster. Before adopting it, check how Split distributes the remainder against your invoicing rules, and confirm that your database driver supports the driver.Valuer and sql.Scanner interfaces if you intend to store amounts in a PostgreSQL composite type.
Frequently asked questions
What is the bojanz/currency Go library?
It handles currency amounts, provides currency information and formatting, and is powered by CLDR v48 in about 40kb of data. It offers currency codes, symbols and formats for all locales, a country to currency mapping, an Amount struct with value semantics and a Formatter for formatting and parsing amounts.
Why does the currency library not use floats or integer cents?
Floats cannot represent currency amounts accurately, and integer minor units become awkward with multiple currencies and with sub-minor-unit precision. The library therefore uses an arbitrary-precision decimal type from the cockroachdb/apd package, wrapped in an Amount struct so the implementation can be replaced later without a breaking change.
How does currency.Split distribute the remainder?
The README example shows 1103.92 split into three installments as 367.98, 367.97 and 367.97, so the remainder goes into the first installment rather than being dropped or spread evenly. Check that against your invoicing rules before relying on it.
Can currency amounts be stored in PostgreSQL?
Yes. Amount implements driver.Valuer and sql.Scanner, so with the pgx driver the number and the currency code can be stored together in a composite type, as in the example schema in the documentation. Integration notes for MySQL, MariaDB and SQLite are in the project wiki.
Does the currency library include currency names?
No. The documentation explains that names are rarely shown but take significant space, so they are left out of the data and can instead be fetched on the frontend through the browser's Intl.DisplayNames.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/bojanz-currency)Was this useful?