A behavioral port of decimal.js v10.6.0: exact decimal math for money, rates and measurements — zero dependencies, Go 1.25+, and verified byte-for-byte against the original.
go get github.com/iSundram/decimal-go
package main
import (
"fmt"
"github.com/iSundram/decimal-go"
)
func main() {
// Money math without the float rounding surprises of float64.
price := decimal.New("19.99")
total := price.Times(decimal.New("3"))
fmt.Println(total) // 59.97
fmt.Println(decimal.New("0.1").Plus(decimal.New("0.2"))) // 0.3
// Custom precision via a cloned constructor.
c := decimal.Default.Clone(&decimal.Config{Precision: decimal.I64(50)})
pi := c.New("3.14159265358979323846264338327950288419716939937510")
fmt.Println(pi.ToFixed(30))
}
59.97
0.3
3.141592653589793238462643383280
x.Div(y), x.Sqrt(), x.Pow(y), x.Sin() —
short names, immutable values, every op returns a new Decimal.
Every decimal.js long-form name — DividedBy, NaturalLogarithm,
SquareRoot, … — exists (39 as aliases, Plus/Minus/Times/ValueOf
as primary names), so migrated JS code compiles almost verbatim.
Clone a constructor per goroutine — precision, rounding and exponent bounds live on it, race-free by design.
encoding.TextMarshaler, JSON (MarshalJSON/ToJSON),
and database/sql Scan/Value.
Invalid input panics with a [DecimalError]-prefixed message —
the direct Go analogue of decimal.js throwing.
The standard library only. All arithmetic is hand-rolled on base-1e7 coefficient words, exactly like the original.
Parity with the real decimal.js is checked mechanically, on every push:
decimal.go run) and the live decimal.js (node);
outputs are byte-for-byte identical: 1518/1518 result lines
(66 inputs × 23 ops, xvalidate/).-race.[DecimalError] panics are
permitted, anything else fails the build (fuzz_test.go).log(0, base) for 0 < base < 1, and
a toFraction() hang under rounding: 3 — preserved
for parity until upstream fixes them. Rationale in
docs/DECISIONS.md.
Same machine, Node v22 vs Go 1.25, op-only operands at the default precision 20. Ratio < 1 means the Go port is faster. Full table and methodology in bench/README.md.
| Op | Go ns/op | decimal.js ns/op | Go/JS |
|---|---|---|---|
| Mul | 332 | 1039 | 0.32 |
| Div @1,000 digits | 20 416 | 52 159 | 0.39 |
| Cbrt | 11 341 | 26 129 | 0.43 |
| Div | 1 184 | 2 468 | 0.48 |
| Sqrt | 8 082 | 11 829 | 0.68 |
| Exp | 40 885 | 56 694 | 0.72 |
| Add | 267 | 323 | 0.83 |
| New(string) — parse | 1 642 | 1 133 | 1.45 |
| Parse → op → format round trip | 3 047 | 1 768 | 1.72 |
23 of 25 directly comparable pairs are faster in Go (up to ~3× on multiplication/division). The two slower paths are string parsing and round trips that re-parse inputs — V8's JIT is extremely good at that workload, while Go pays per-parse allocation overhead. We disclose rather than hide this.
Every decimal.js op → Go: rounding semantics, edge cases, code locations. MATRIX.md
Method-name mapping, construction semantics, concurrency, errors. Migration guide
Why base-1e7, why panic not error, why clones, why the quirks stay. DECISIONS.md
Full package docs with runnable examples on pkg.go.dev.