Arbitrary-precision fixed-point decimal numbers for Go, avoiding floating-point precision issues with support for arithmetic, rounding, serialization, and database integration.
Common pitfalls and how to avoid them when working with shopspring/decimal.
❌ WRONG:
a := decimal.NewFromFloat(1.0)
b := decimal.NewFromFloat(1.0)
if a == b { // DON'T DO THIS!
// May not work as expected
}✅ CORRECT:
a := decimal.NewFromFloat(1.0)
b := decimal.NewFromFloat(1.0)
if a.Equal(b) { // Use .Equal()
// Correct comparison
}
// Or use .Cmp()
if a.Cmp(b) == 0 {
// Also correct
}Why:
== compares struct fields, including pointer addresses.Equal() or .Cmp()❌ WRONG:
result := a.Div(b) // Panics if b is zero!✅ CORRECT:
if b.IsZero() {
return decimal.Zero, errors.New("division by zero")
}
result := a.Div(b)Also check:
// QuoRem also panics on zero
q, r := a.QuoRem(b, 0) // Panics if b is zero
// Mod also panics on zero
remainder := a.Mod(b) // Panics if b is zero❌ WRONG:
d := decimal.NewFromFloat(userFloat) // May panic!✅ CORRECT:
if math.IsNaN(userFloat) || math.IsInf(userFloat, 0) {
return decimal.Zero, errors.New("invalid float value")
}
d := decimal.NewFromFloat(userFloat)Applies to:
NewFromFloat()NewFromFloat32()NewFromFloatWithExponent()⚠ CAUTION:
// May lose precision
d := decimal.NewFromFloat(0.1)
fmt.Println(d.String()) // May be "0.1000000000000000055..."✓ RECOMMENDED:
// Exact precision
d, err := decimal.NewFromString("0.1")
fmt.Println(d.String()) // "0.1"When to use each:
NewFromString - Financial values, user input, exact valuesNewFromFloat - Converting existing float64, approximate values OK❌ WRONG ASSUMPTION:
a := decimal.NewFromInt(10)
a.Add(decimal.NewFromInt(5)) // Returns new value, doesn't modify a
fmt.Println(a) // Still 10!✅ CORRECT:
a := decimal.NewFromInt(10)
a = a.Add(decimal.NewFromInt(5)) // Assign result
fmt.Println(a) // 15All operations return new values:
a := decimal.NewFromInt(10)
b := a.Add(decimal.NewFromInt(5)) // a is still 10, b is 15
c := a.Mul(decimal.NewFromInt(2)) // a is still 10, c is 20✓ SAFE:
var total decimal.Decimal
// total is 0, safe to use
total.IsZero() // true
total.Equal(decimal.Zero) // true
total = total.Add(someValue) // Works fineNo need to initialize:
type Order struct {
Total decimal.Decimal // Zero value is 0, not nil
}
var order Order
order.Total.Add(price) // Safe, no nil panic⚠ NOT THREAD-SAFE:
// Bad: Race condition in concurrent code
func goroutine1() {
decimal.DivisionPrecision = 4
result := a.Div(b) // May use wrong precision!
}
func goroutine2() {
decimal.DivisionPrecision = 16
result := a.Div(b)
}✓ SAFE:
// Set once at startup
func init() {
decimal.DivisionPrecision = 4
decimal.MarshalJSONWithoutQuotes = false
}
// Or use DivRound for per-operation precision
result := a.DivRound(b, 4) // Thread-safe⚠ CAUTION:
decimal.MarshalJSONWithoutQuotes = true
large := decimal.NewFromString("99999999999999999")
data, _ := json.Marshal(large)
// JSON: 99999999999999999
// JavaScript receives: 100000000000000000 (precision lost!)✓ SAFE (default):
decimal.MarshalJSONWithoutQuotes = false // Default
large := decimal.NewFromString("99999999999999999")
data, _ := json.Marshal(large)
// JSON: "99999999999999999" (quoted, precision preserved)✓ DO:
// Exact values from strings
price, err := decimal.NewFromString("19.99")
// Integer values
quantity := decimal.NewFromInt(10)
// Constants (panic is OK)
const TAX_RATE = "0.08"
taxRate := decimal.RequireFromString(TAX_RATE)✗ AVOID:
// Floats for financial values
price := decimal.NewFromFloat(19.99) // May be imprecise
// Ignoring errors
price, _ := decimal.NewFromString(userInput) // Check error!✓ DO:
// Check divisor
if divisor.IsZero() {
return handleDivisionByZero()
}
result := dividend.Div(divisor)
// Round financial results
taxAmount := subtotal.Mul(taxRate).Round(2)
// Chain operations for readability
total := subtotal.
Mul(quantity).
Add(shipping).
Sub(discount).
Round(2)✗ AVOID:
// Unchecked division
result := a.Div(b) // Panics if b is zero
// Forgetting to round financial values
taxAmount := subtotal.Mul(taxRate) // Should round to 2 places
// Complex one-liners
result := a.Add(b).Mul(c).Div(d).Sub(e).Add(f) // Hard to debug✓ DO:
if price.GreaterThan(decimal.Zero) {
// Positive price
}
if discount.Equal(decimal.Zero) {
// No discount
}
switch amount.Cmp(limit) {
case -1:
// amount < limit
case 0:
// amount == limit
case 1:
// amount > limit
}✗ AVOID:
if price == decimal.Zero { // WRONG!
// Don't use ==
}
if price > limit { // WRONG!
// Can't use >
}✓ DO:
// Use NUMERIC/DECIMAL types
CREATE TABLE products (
price NUMERIC(10, 2) NOT NULL
);
// Use NullDecimal for nullable columns
type Product struct {
Price decimal.Decimal
Discount decimal.NullDecimal // Can be NULL
}
// Check Valid field
if product.Discount.Valid {
applyDiscount(product.Discount.Decimal)
}✗ AVOID:
// Don't use FLOAT/REAL for financial data
CREATE TABLE products (
price REAL -- Loses precision!
);
// Don't ignore Valid field
amount := product.Discount.Decimal // May be zero when actually NULL✓ DO:
// Keep default (quoted strings)
decimal.MarshalJSONWithoutQuotes = false
// Validate after unmarshal
var req CreateProductRequest
json.Unmarshal(data, &req)
if req.Price.LessThanOrEqual(decimal.Zero) {
return errors.New("invalid price")
}
// Use NullDecimal for optional values
type Product struct {
Price decimal.Decimal `json:"price"`
Discount decimal.NullDecimal `json:"discount"`
}✗ AVOID:
// Enabling unquoted numbers without understanding risk
decimal.MarshalJSONWithoutQuotes = true // Precision loss risk!
// Not validating unmarshaled values
json.Unmarshal(data, &req)
processPayment(req.Amount) // Could be negative, zero, or invalid!✓ DO:
// Use appropriate rounding for use case
price := calculated.Round(2) // Financial (half-up)
fairRounded := calculated.RoundBank(2) // Statistical (banker's)
cash := calculated.RoundCash(5) // Point of sale
// Round after calculations
result := a.Mul(b).Add(c).Div(d).Round(2)
// Document rounding strategy
// Always rounds using banker's rounding to 2 decimal places
amount := calculated.RoundBank(2)✗ AVOID:
// Inconsistent rounding
amount1 := calc1.Round(2)
amount2 := calc2.RoundBank(2) // Different strategies!
// Rounding too early
rounded := a.Round(2)
result := rounded.Mul(b).Div(c) // Accumulated rounding errors✓ DO:
// Check constructor errors
price, err := decimal.NewFromString(input)
if err != nil {
return fmt.Errorf("invalid price: %w", err)
}
// Check before division
if quantity.IsZero() {
return decimal.Zero, errors.New("cannot divide by zero quantity")
}
unitPrice := totalPrice.Div(quantity)
// Validate float inputs
if math.IsNaN(f) || math.IsInf(f, 0) {
return decimal.Zero, errors.New("invalid float")
}
d := decimal.NewFromFloat(f)
// Check method errors
result, err := d.PowInt32(exp)
if err != nil {
return decimal.Zero, fmt.Errorf("power calculation failed: %w", err)
}✗ AVOID:
// Ignoring errors
price, _ := decimal.NewFromString(userInput)
// No division by zero check
result := a.Div(b) // Panics!
// Unchecked float conversion
d := decimal.NewFromFloat(userFloat) // May panic on NaN/Inf✓ FASTER:
hundred := decimal.NewFromInt(100)
// Reuse hundred
percent1 := value1.Div(hundred)
percent2 := value2.Div(hundred)
percent3 := value3.Div(hundred)✗ SLOWER:
percent1 := value1.Div(decimal.NewFromInt(100))
percent2 := value2.Div(decimal.NewFromInt(100))
percent3 := value3.Div(decimal.NewFromInt(100))✓ FASTER:
// Multiply by 100
result := value.Shift(2)
// Divide by 1000
result := value.Shift(-3)✗ SLOWER:
result := value.Mul(decimal.NewFromInt(100))
result := value.Div(decimal.NewFromInt(1000))✓ MOST ACCURATE:
price, err := decimal.NewFromString("19.99") // Exact✗ LESS ACCURATE:
price := decimal.NewFromFloat(19.99) // May be imprecise
price := decimal.New(1999, -2) // Requires calculationPerformance vs Accuracy:
New() - Fastest, requires understanding exponentsNewFromInt() - Fast, safe for integersNewFromFloat() - Fast, may lose precisionNewFromString() - Slower, most accurate✓ BETTER:
result := a.DivRound(b, 2)✗ LESS EFFICIENT:
result := a.Div(b).Round(2)func TestCalculation(t *testing.T) {
result := calculateTotal(items)
want := decimal.NewFromString("99.99")
if !result.Equal(want) {
t.Errorf("got %s, want %s", result, want)
}
}func TestDivision(t *testing.T) {
tests := []struct {
name string
a, b decimal.Decimal
want decimal.Decimal
wantErr bool
}{
{
name: "normal division",
a: decimal.NewFromInt(10),
b: decimal.NewFromInt(2),
want: decimal.NewFromInt(5),
},
{
name: "division by zero",
a: decimal.NewFromInt(10),
b: decimal.Zero,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.b.IsZero() {
// Expect panic
defer func() {
if r := recover(); r == nil {
t.Error("expected panic")
}
}()
}
got := tt.a.Div(tt.b)
if !tt.wantErr && !got.Equal(tt.want) {
t.Errorf("got %s, want %s", got, tt.want)
}
})
}
}func TestPrecision(t *testing.T) {
// Test that precision is maintained
price := decimal.NewFromString("19.99")
tax := price.Mul(decimal.NewFromString("0.08")).Round(2)
// Verify exact value
want := decimal.NewFromString("1.60")
if !tax.Equal(want) {
t.Errorf("tax calculation: got %s, want %s", tax, want)
}
// Also verify string representation
if tax.StringFixed(2) != "1.60" {
t.Errorf("tax string: got %s, want 1.60", tax.StringFixed(2))
}
}_, err := decimal.NewFromString("invalid")
// Error: can't convert invalid to decimalSolution: Validate input before parsing
result := a.Div(decimal.Zero)
// Panic: division by zeroSolution: Check IsZero() before dividing
d := decimal.NewFromFloat(math.NaN())
// Panic: Invalid argument to NewFromFloatSolution: Check for NaN/Inf before calling
// If MarshalJSONWithoutQuotes = true
// But unmarshaling into string fieldSolution: Keep consistent JSON format
Before:
var price float64 = 19.99
total := price * quantityAfter:
price := decimal.NewFromString("19.99")
total := price.Mul(decimal.NewFromInt(quantity))Before:
priceCents := int64(1999) // $19.99
dollars := float64(priceCents) / 100After:
priceCents := decimal.NewFromInt(1999)
dollars := priceCents.Shift(-2) // 19.99Before:
a := big.NewFloat(19.99)
b := big.NewFloat(1.08)
c := new(big.Float).Mul(a, b)After:
a := decimal.NewFromString("19.99")
b := decimal.NewFromString("1.08")
c := a.Mul(b)== or != on Decimal valuesInstall with Tessl CLI
npx tessl i tessl/golang-github-com-shopspring--decimal