decimal-go

Arbitrary-precision decimal arithmetic, ported to Go

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 1.25+ MIT License Zero dependencies 61/61 test modules ported 1518 result lines cross-validated Race detector clean 96% coverage

Install

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

Usage at a glance

Idiomatic Go API

x.Div(y), x.Sqrt(), x.Pow(y), x.Sin() — short names, immutable values, every op returns a new Decimal.

decimal.js parity aliases

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.

Per-context constructors

Clone a constructor per goroutine — precision, rounding and exponent bounds live on it, race-free by design.

Serialization built-in

encoding.TextMarshaler, JSON (MarshalJSON/ToJSON), and database/sql Scan/Value.

Errors in one place

Invalid input panics with a [DecimalError]-prefixed message — the direct Go analogue of decimal.js throwing.

Zero dependencies

The standard library only. All arithmetic is hand-rolled on base-1e7 coefficient words, exactly like the original.

Behavioral parity — proven, not claimed

Parity with the real decimal.js is checked mechanically, on every push:

The port deliberately keeps two decimal.js quirks rather than "fixing" them: the 1024-significant-digit precision limit on transcendental constants, and construction-time non-rounding (a value keeps its full coefficient until an op rounds it). It also inherits two bugs found upstream by differential fuzzing — 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.

Benchmarks — honestly

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.

OpGo ns/opdecimal.js ns/opGo/JS
Mul33210390.32
Div @1,000 digits20 41652 1590.39
Cbrt11 34126 1290.43
Div1 1842 4680.48
Sqrt8 08211 8290.68
Exp40 88556 6940.72
Add2673230.83
New(string) — parse1 6421 1331.45
Parse → op → format round trip3 0471 7681.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.

Documentation

Operations matrix

Every decimal.js op → Go: rounding semantics, edge cases, code locations. MATRIX.md

Migrating from decimal.js

Method-name mapping, construction semantics, concurrency, errors. Migration guide

Design decisions

Why base-1e7, why panic not error, why clones, why the quirks stay. DECISIONS.md

API reference

Full package docs with runnable examples on pkg.go.dev.