Today, we’re releasing our arbitrary-precision decimal library for Go.

You can get it by running: go get github.com/shopspring/decimal

Motivation

When dealing with money, rounding errors are undesirable. Unfortunately, floating point numbers can’t represent fractional decimals (ex: 0.1) precisely, so adding and subtracting them repeatedly will cause rounding errors. For example, consider this code:

You might expect it to print 10 , but in fact it prints 9.999999999999831 . Over time, these errors can accumulate, and eventually you might be missing a few dollars with no idea where it went. That’s why most accounting systems use decimal numbers when dealing with money.

Why we had to write our own

Unfortunately, Go doesn’t have a built-in decimal library. The best option we found was fpd.Decimal. However, we quickly ran into many issues:

Lack of features — fpd.Decimal didn’t support creating a Decimal by parsing a string.

Bugs — Decimal.Add isn’t commutative, and sometimes loses precision.

Strange API — Decimal.String() returns the string representation of the decimal, but without the decimal point.

Dangerous — operating on an uninitialized Decimal causes a nil pointer panic. When you have many Decimals as part of a struct, making sure they’re always initialized is a pain.

We had to rewrite most of fpd.Decimal and change its API significantly to make it usable for our purposes. Because the API changes broke backwards compatibility, we decided to release it as a separate open source library.

How our library works

A decimal number is represented by an integer coefficient and an integer exponent, with the number = coefficient * 10 ^ exponent .

Addition/Subtraction converts the decimal with the higher exponent to an equivalent decimal with an exponent equal to the other decimal, then adds the coefficients together.

Multiplication multiplies the coefficients and adds the exponents: (a1 * 10 ^ e1) * (a2 * 10 ^ e2) = (a1 * a2) * 10 ^ (e1 + e2)

Division is sometimes impossible without a loss of precision. For example, 1 / 3 cannot be represented with a finite number of decimal digits. Decimal libraries typically allow you to specify a precision for division to handle cases like this. Division is tricky to implement so we sacrificed execution speed for correctness and used Go’s big.Rat library. We convert the decimals to big.Rat, convert the result of the division to a string and parse it to get the resultant decimal.

Before each operation, we ensure the decimal is initialized, thus decimal’s zero-value is 0 as you’d expect.

Final thoughts

We believe our decimal library offers a clean API that’s consistent with other Go packages such as big.Int. The mathematical operations are implemented correctly and are tested thoroughly. The zero-value of a Decimal is 0, just like a regular int or float64, which makes it easy to use within structs. You can serialize/deserialize to JSON, XML, or your SQL database without writing any additional code. And, it’s been used in production at Spring for over 6 months with no issues.

We are committed to using this open-sourced version internally, and we’ll release all fixes and improvements publicly. We hope that you’ll find this library useful, and we welcome pull requests!