Arbitrary-precision fixed-point decimal numbers for Go, avoiding floating-point precision issues with support for arithmetic, rounding, serialization, and database integration.
Arbitrary-precision fixed-point decimal numbers for Go. Eliminates floating-point precision errors in financial calculations, e-commerce, and any application requiring exact decimal arithmetic.
Package: github.com/shopspring/decimal | Version: v1.4.0 | Install: go get github.com/shopspring/decimal
import "github.com/shopspring/decimal"
// Create decimals (ALWAYS prefer NewFromString for exact values)
price, _ := decimal.NewFromString("19.99")
quantity := decimal.NewFromInt(3)
// Calculate (immutable - returns new values)
total := price.Mul(quantity) // 59.97
// Compare (NEVER use == operator - won't work!)
if total.GreaterThan(decimal.NewFromInt(50)) {
discount := total.Mul(decimal.NewFromString("0.10"))
total = total.Sub(discount)
}
// Round and display
fmt.Println(total.StringFixed(2)) // "53.97"| ❌ NEVER DO THIS | ✅ ALWAYS DO THIS | Why |
|---|---|---|
if d1 == d2 | if d1.Equal(d2) | == compares pointers, not values - will fail |
result := d.Div(divisor) | if !divisor.IsZero() { result = d.Div(divisor) } | Division by zero causes panic |
decimal.NewFromFloat(19.99) | decimal.NewFromString("19.99") | Float has precision loss: 19.989999... |
d.Add(x) (expecting d to change) | d = d.Add(x) (assign result) | Decimal is immutable - operations return new values |
MarshalJSONWithoutQuotes=true | Keep false (default) | Unquoted JSON numbers lose precision in JavaScript |
Ignoring NewFromString error | Always check err != nil | Invalid strings return errors you must handle |
NewFromString("19.99") → Guide | APINewFromInt(100) → GuideNewFromFloat(3.14) → Guide (caution: precision loss)NullDecimal → Guide | APIEqual(), GreaterThan(), LessThan() → APIIsZero(), IsPositive(), IsNegative() → APICmp() returns -1/0/1 → APIStringFixed(2) → GuideStringFixedCash(5) → GuideNullDecimal → Guide | APINullDecimal with omitempty → JSON Guide| Constructor | Use When | Precision | Example |
|---|---|---|---|
NewFromString(s) | Exact values needed ⭐ | Perfect | NewFromString("19.99") |
NewFromInt(i) | Integer values | Perfect | NewFromInt(100) |
NewFromInt32(i) | 32-bit integers | Perfect | NewFromInt32(42) |
NewFromFloat(f) | Converting existing float ⚠️ | May lose | NewFromFloat(3.14) |
New(value, exp) | Manual control: value × 10^exp | Perfect | New(1999, -2) = 19.99 |
Zero | Need zero constant | Perfect | decimal.Zero |
Complete Constructor Guide → | Constructor API →
| Operation | Method | Returns | Example | Notes |
|---|---|---|---|---|
| Addition | Add(d2) | d + d2 | price.Add(tax) | |
| Subtraction | Sub(d2) | d - d2 | total.Sub(discount) | |
| Multiplication | Mul(d2) | d × d2 | price.Mul(quantity) | |
| Division | Div(d2) | d ÷ d2 | total.Div(count) | ⚠️ Panics if d2=0 |
| Division (rounded) | DivRound(d2, places) | Rounded d ÷ d2 | amount.DivRound(decimal.NewFromInt(3), 2) | Safer than Div |
| Modulo | Mod(d2) | d % d2 | amount.Mod(interval) | |
| Negation | Neg() | -d | profit.Neg() (make loss) | |
| Absolute value | Abs() | |d| | delta.Abs() | |
| Power of 10 shift | Shift(places) | d × 10^places | amount.Shift(2) (×100) | Fast multiplication/division |
Complete Arithmetic Guide → | Arithmetic API →
| Method | Returns | Use For | Example |
|---|---|---|---|
Equal(d2) | bool | Exact equality | price.Equal(decimal.NewFromString("19.99")) |
Cmp(d2) | -1, 0, 1 | Three-way compare, sorting | if total.Cmp(limit) > 0 |
GreaterThan(d2) | bool | d > d2 | if balance.GreaterThan(minBalance) |
GreaterThanOrEqual(d2) | bool | d ≥ d2 | if amount.GreaterThanOrEqual(threshold) |
LessThan(d2) | bool | d < d2 | if price.LessThan(maxPrice) |
LessThanOrEqual(d2) | bool | d ≤ d2 | if value.LessThanOrEqual(cap) |
IsZero() | bool | d == 0 | if balance.IsZero() |
IsPositive() | bool | d > 0 | if profit.IsPositive() |
IsNegative() | bool | d < 0 | if loss.IsNegative() |
| Method | Strategy | 2.5 → | -2.5 → | Use Case |
|---|---|---|---|---|
Round(n) | Half-up (away from 0) | 3 | -3 | General purpose ⭐ Most common |
RoundBank(n) | Banker's (to even) | 2 | -2 | Minimize rounding bias in large datasets |
RoundUp(n) | Away from zero | 3 | -3 | Conservative estimates (always round up) |
RoundDown(n) | Toward zero | 2 | -2 | Aggressive estimates (always round down) |
RoundCeil(n) | Toward +∞ | 3 | -2 | Ceiling function |
RoundFloor(n) | Toward -∞ | 2 | -3 | Floor function |
Truncate(n) | Cut off digits | 2 | -2 | No rounding, just truncate |
Rounding Decision Guide → | Rounding API →
| Method | Output | Use Case | Example |
|---|---|---|---|
String() | Full precision | Debug, logging | "19.9900" |
StringFixed(places) | Fixed decimals (half-up) | Money display ⭐ | StringFixed(2) → "19.99" |
StringFixedBank(places) | Fixed decimals (banker's) | Financial reports | StringFixedBank(2) → "19.98" |
StringFixedCash(interval) | Cash rounding | Physical currency | StringFixedCash(5) → "20.00" |
Complete Formatting Guide → | Conversion API →
| Method | To Type | Precision | Notes |
|---|---|---|---|
String() | string | Perfect | Full precision preserved |
StringFixed(n) | string | n decimal places | Rounded to n places |
Float64() | (float64, bool) | May lose ⚠️ | Returns (value, exact) - exact=false if precision lost |
IntPart() | int64 | Integer only | Drops fractional part, may overflow |
BigInt() | *big.Int | Integer only | Drops fractional part, no overflow |
BigFloat() | *big.Float | May lose | Approximate conversion |
Rat() | *big.Rat | Perfect | Exact rational representation |
// Calculate price with tax
price, _ := decimal.NewFromString("99.99")
taxRate, _ := decimal.NewFromString("0.08") // 8% tax
// Calculate tax amount
tax := price.Mul(taxRate).Round(2)
// Calculate total
total := price.Add(tax)
fmt.Printf("Price: $%s\n", price.StringFixed(2)) // $99.99
fmt.Printf("Tax: $%s\n", tax.StringFixed(2)) // $8.00
fmt.Printf("Total: $%s\n", total.StringFixed(2)) // $107.99// Apply percentage discount
originalPrice, _ := decimal.NewFromString("200.00")
discountPercent := decimal.NewFromInt(15) // 15% off
hundred := decimal.NewFromInt(100)
// Calculate discount amount
discountAmount := originalPrice.Mul(discountPercent).Div(hundred)
// Apply discount and round
finalPrice := originalPrice.Sub(discountAmount).Round(2)
fmt.Printf("Original: $%s\n", originalPrice.StringFixed(2)) // $200.00
fmt.Printf("Discount: $%s (15%%)\n", discountAmount.StringFixed(2)) // $30.00
fmt.Printf("Final: $%s\n", finalPrice.StringFixed(2)) // $170.00// Production-ready division function
func SafeDivide(numerator, denominator decimal.Decimal, places int32) (decimal.Decimal, error) {
if denominator.IsZero() {
return decimal.Zero, errors.New("division by zero")
}
return numerator.DivRound(denominator, places), nil
}
// Usage
average, err := SafeDivide(total, count, 2)
if err != nil {
return fmt.Errorf("cannot calculate average: %w", err)
}// Product with decimal fields
type Product struct {
ID int64 `db:"id"`
Name string `db:"name"`
Price decimal.Decimal `db:"price"` // Required field
Discount decimal.NullDecimal `db:"discount"` // Optional field (can be NULL)
}
// Insert product
_, err := db.Exec(
"INSERT INTO products (id, name, price, discount) VALUES (?, ?, ?, ?)",
product.ID,
product.Name,
product.Price,
product.Discount, // NullDecimal handles NULL automatically
)
// Query product
var product Product
err := db.QueryRow(
"SELECT id, name, price, discount FROM products WHERE id = ?",
productID,
).Scan(&product.ID, &product.Name, &product.Price, &product.Discount)
// Check if discount is NULL
if product.Discount.Valid {
finalPrice := product.Price.Sub(product.Discount.Decimal)
} else {
finalPrice := product.Price
}// API response structure
type OrderResponse struct {
OrderID int64 `json:"order_id"`
Subtotal decimal.Decimal `json:"subtotal"` // Serializes as "19.99" (quoted)
Tax decimal.Decimal `json:"tax"`
Total decimal.Decimal `json:"total"`
Discount decimal.NullDecimal `json:"discount,omitempty"` // null or omitted if not set
}
// Create response
order := OrderResponse{
OrderID: 12345,
Subtotal: decimal.NewFromString("89.97"),
Tax: decimal.NewFromString("7.20"),
Total: decimal.NewFromString("97.17"),
Discount: decimal.NullDecimal{Valid: false}, // No discount
}
// Marshal to JSON (default: quoted strings for precision)
jsonData, err := json.Marshal(order)
// {"order_id":12345,"subtotal":"89.97","tax":"7.20","total":"97.17"}
// Unmarshal from JSON
var received OrderResponse
err = json.Unmarshal(jsonData, &received)// Split amount among N recipients with remainder handling
func SplitAmount(total decimal.Decimal, recipients int) ([]decimal.Decimal, error) {
if recipients <= 0 {
return nil, errors.New("recipients must be positive")
}
recipientsDecimal := decimal.NewFromInt(int64(recipients))
perPerson := total.DivRound(recipientsDecimal, 2)
// Calculate remainder (due to rounding)
distributed := perPerson.Mul(recipientsDecimal)
remainder := total.Sub(distributed)
// Create shares
shares := make([]decimal.Decimal, recipients)
for i := 0; i < recipients; i++ {
shares[i] = perPerson
}
// Add remainder to first person (or distribute across recipients)
if !remainder.IsZero() {
shares[0] = shares[0].Add(remainder)
}
return shares, nil
}
// Usage: Split $100 among 3 people
shares, _ := SplitAmount(decimal.NewFromString("100.00"), 3)
// shares[0] = 33.34, shares[1] = 33.33, shares[2] = 33.33| Error/Issue | Cause | Solution |
|---|---|---|
| Comparison doesn't work | Used == or != operator | Use .Equal(d2) method instead |
| Panic: division by zero | Divisor is zero | Check divisor.IsZero() before dividing |
| Panic: NaN or Inf | Used NewFromFloat(NaN) or NewFromFloat(Inf) | Validate float before converting |
| Lost precision in calculation | Used NewFromFloat with literal | Use NewFromString("19.99") instead |
| Wrong rounding result | Used wrong rounding method | See rounding decision tree |
| JSON precision loss | Set MarshalJSONWithoutQuotes=true | Keep default false - use quoted strings |
| Division not precise enough | Default 16 digits insufficient | Set decimal.DivisionPrecision or use DivRound |
| NULL database value error | Used Decimal for nullable column | Use NullDecimal type instead |
| Decimal not changing after operation | Forgot immutability | Assign result: d = d.Add(x) |
| Decimal comparison sorting wrong | Using wrong comparison | Use Cmp() method: returns -1/0/1 |
// Immutable arbitrary-precision decimal number
// Internally: coefficient × 10^exponent
// Zero value is valid and equals 0
type Decimal struct { /* internal fields */ }Key Properties:
var d decimal.Decimal equals 0 and is ready to use1999 × 10^-2 = 19.99// Nullable decimal for database NULL and optional API fields
type NullDecimal struct {
Decimal Decimal
Valid bool // true if Decimal is valid (not NULL)
}Use For:
omitempty)// Create NULL value
nullValue := decimal.NullDecimal{Valid: false}
// Create valid value
validValue := decimal.NullDecimal{
Decimal: decimal.NewFromString("19.99"),
Valid: true,
}
// Check before using
if validValue.Valid {
price := validValue.Decimal
}Complete NullDecimal Guide → | Database Integration →
// Package-level variables (NOT thread-safe - set once at startup)
var DivisionPrecision = 16 // Decimal places for Div() operation
var PowPrecisionNegativeExponent = 16 // Precision for Pow() with negative exponent
var MarshalJSONWithoutQuotes = false // JSON: false="19.99" (recommended), true=19.99 (loses precision)
var ExpMaxIterations = 1000 // Max iterations for ExpHullAbrham method
var Zero = New(0, 1) // Reusable zero constant⚠️ Warning: These are global mutable variables. Set once during application initialization, NOT at runtime. Not thread-safe to modify.
// Example: Configure at startup
func init() {
decimal.DivisionPrecision = 8 // Use 8 decimal places for division
decimal.MarshalJSONWithoutQuotes = false // Keep quoted (recommended)
}| Format | Marshal | Unmarshal | Default Encoding |
|---|---|---|---|
| JSON | ✓ | ✓ | Quoted string: "19.99" (preserves precision) |
| XML | ✓ | ✓ | String in XML text element |
| Binary | ✓ | ✓ | Custom binary format (compact) |
| Gob | ✓ | ✓ | Go gob encoding |
| SQL | ✓ (Value) | ✓ (Scan) | String or numeric (driver-dependent) |
Complete Serialization Guide → | JSON Guide → | Database Guide →
float64 if small precision loss is acceptablefloat64, float32, or math/big.Floatint64 or int (faster)float32 or float64 (much faster)math/big.Float or math/big.RatE-commerce: Creating → Arithmetic Patterns → Formatting → JSON
Database: Integration Guide → NULL Handling → Storage Recommendations
Financial: Arithmetic → Financial Patterns → Rounding → Best Practices
Migration: Migration Guide - Replace types/operators, handle division by zero, testing patterns
Internal: coefficient × 10^exponent (e.g., 19.99 = 1999 × 10^-2). Coefficient uses *big.Int (arbitrary precision), exponent is int32. Zero value Decimal{} equals 0.
Performance: String parsing (slow, accurate) vs integer ops (fast) vs float conversion (fast, may lose precision). Immutable = new allocation per operation.
Thread Safety: Decimal/NullDecimal values are thread-safe (immutable). Global config vars are NOT thread-safe (set once at init).
Common issues: Comparison not working? Use .Equal() not ==. Panic? Check .IsZero() before division. Wrong value? Use NewFromString() not NewFromFloat(). NULL handling? Use NullDecimal.
Resources: Gotchas · Common Errors · Best Practices
Install with Tessl CLI
npx tessl i tessl/golang-github-com-shopspring--decimal@1.4.1