Arbitrary-precision fixed-point decimal numbers for Go, avoiding floating-point precision issues with support for arithmetic, rounding, serialization, and database integration.
Complete guide to using Decimal with database/sql and handling NULL values.
import (
"database/sql"
"github.com/shopspring/decimal"
_ "github.com/lib/pq" // or your database driver
)
type Product struct {
ID int
Price decimal.Decimal
}
// Query
var p Product
err := db.QueryRow("SELECT id, price FROM products WHERE id = $1", 1).
Scan(&p.ID, &p.Price)
// Insert
_, err = db.Exec("INSERT INTO products (price) VALUES ($1)", p.Price)type Product struct {
ID int
Price decimal.Decimal
Discount decimal.NullDecimal // Can be NULL
}
// Query with NULL handling
var p Product
err := db.QueryRow("SELECT id, price, discount FROM products WHERE id = $1", 1).
Scan(&p.ID, &p.Price, &p.Discount)
if p.Discount.Valid {
fmt.Println("Discount:", p.Discount.Decimal)
} else {
fmt.Println("No discount")
}
// Insert NULL
var discount decimal.NullDecimal // Valid=false means NULL
_, err = db.Exec("INSERT INTO products (discount) VALUES ($1)", discount)Decimal values are stored as strings in the database via the driver.Value interface.
Why strings?
Recommended database types:
NUMERIC, DECIMALDECIMAL, NUMERICTEXT, REALDECIMAL, NUMERIC-- PostgreSQL / MySQL
CREATE TABLE products (
id SERIAL PRIMARY KEY,
price NUMERIC(10, 2) NOT NULL, -- Non-nullable
discount NUMERIC(10, 2), -- Nullable
cost NUMERIC(10, 4) -- Higher precision
);
-- SQLite
CREATE TABLE products (
id INTEGER PRIMARY KEY,
price TEXT NOT NULL, -- Store as string
discount TEXT, -- Nullable string
cost TEXT
);// Scan implements sql.Scanner for reading from database
func (d *Decimal) Scan(value interface{}) error
// Value implements driver.Valuer for writing to database
func (d Decimal) Value() (driver.Value, error)Accepted types in Scan:
string - Most common, preserves precision[]byte - String as bytesint64 - Integer valuesfloat64 - Float values (may lose precision)Value returns:
string - Decimal formatted as stringtype NullDecimal struct {
Decimal Decimal
Valid bool // true if not NULL
}
// Scan implements sql.Scanner
func (d *NullDecimal) Scan(value interface{}) error
// Value implements driver.Valuer
func (d NullDecimal) Value() (driver.Value, error)Value returns:
nil - when Valid=false (SQL NULL)string - when Valid=true (decimal value)func getProduct(db *sql.DB, id int) (*Product, error) {
var p Product
err := db.QueryRow(`
SELECT id, name, price, discount
FROM products
WHERE id = $1
`, id).Scan(&p.ID, &p.Name, &p.Price, &p.Discount)
if err != nil {
return nil, fmt.Errorf("query product: %w", err)
}
return &p, nil
}func listProducts(db *sql.DB) ([]Product, error) {
rows, err := db.Query(`
SELECT id, name, price, discount
FROM products
ORDER BY name
`)
if err != nil {
return nil, fmt.Errorf("query products: %w", err)
}
defer rows.Close()
var products []Product
for rows.Next() {
var p Product
if err := rows.Scan(&p.ID, &p.Name, &p.Price, &p.Discount); err != nil {
return nil, fmt.Errorf("scan product: %w", err)
}
products = append(products, p)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("rows error: %w", err)
}
return products, nil
}func createProduct(db *sql.DB, p *Product) error {
result, err := db.Exec(`
INSERT INTO products (name, price, discount)
VALUES ($1, $2, $3)
`, p.Name, p.Price, p.Discount)
if err != nil {
return fmt.Errorf("insert product: %w", err)
}
id, err := result.LastInsertId()
if err != nil {
return fmt.Errorf("get last insert id: %w", err)
}
p.ID = int(id)
return nil
}func updatePrice(db *sql.DB, id int, newPrice decimal.Decimal) error {
result, err := db.Exec(`
UPDATE products
SET price = $1, updated_at = NOW()
WHERE id = $2
`, newPrice, id)
if err != nil {
return fmt.Errorf("update price: %w", err)
}
rowsAffected, err := result.RowsAffected()
if err != nil {
return fmt.Errorf("get rows affected: %w", err)
}
if rowsAffected == 0 {
return fmt.Errorf("product not found")
}
return nil
}func deleteProduct(db *sql.DB, id int) error {
_, err := db.Exec(`
DELETE FROM products WHERE id = $1
`, id)
if err != nil {
return fmt.Errorf("delete product: %w", err)
}
return nil
}type Order struct {
ID int
Total decimal.Decimal
Discount decimal.NullDecimal // May be NULL
ShippingCost decimal.NullDecimal // May be NULL
}
func calculateFinalTotal(o *Order) decimal.Decimal {
total := o.Total
// Apply discount if present
if o.Discount.Valid {
total = total.Sub(o.Discount.Decimal)
}
// Add shipping if present
if o.ShippingCost.Valid {
total = total.Add(o.ShippingCost.Decimal)
}
return total
}// Valid value
discount := decimal.NewNullDecimal(decimal.NewFromString("10.00"))
// discount.Valid = true, discount.Decimal = 10.00
// NULL value (zero value)
var nullDiscount decimal.NullDecimal
// nullDiscount.Valid = false (represents NULL)
// NULL value (explicit)
nullDiscount = decimal.NullDecimal{
Decimal: decimal.Zero,
Valid: false,
}
// Conditional NULL
var maybeDiscount decimal.NullDecimal
if hasDiscount {
maybeDiscount = decimal.NewNullDecimal(discountAmount)
} else {
maybeDiscount = decimal.NullDecimal{Valid: false}
}// Insert with NULL discount
product := &Product{
Name: "Widget",
Price: decimal.NewFromString("99.99"),
Discount: decimal.NullDecimal{Valid: false}, // NULL
}
db.Exec(`
INSERT INTO products (name, price, discount)
VALUES ($1, $2, $3)
`, product.Name, product.Price, product.Discount)
// discount column will be NULL// Query that may return NULL
var discount decimal.NullDecimal
err := db.QueryRow(`
SELECT discount FROM products WHERE id = $1
`, id).Scan(&discount)
if err != nil {
return err
}
if discount.Valid {
fmt.Printf("Discount: %s\n", discount.Decimal.StringFixed(2))
} else {
fmt.Println("No discount")
}func transferFunds(db *sql.DB, fromID, toID int, amount decimal.Decimal) error {
tx, err := db.Begin()
if err != nil {
return fmt.Errorf("begin transaction: %w", err)
}
defer tx.Rollback() // Rollback if not committed
// Deduct from source
_, err = tx.Exec(`
UPDATE accounts
SET balance = balance - $1
WHERE id = $2 AND balance >= $1
`, amount, fromID)
if err != nil {
return fmt.Errorf("deduct from source: %w", err)
}
// Add to destination
_, err = tx.Exec(`
UPDATE accounts
SET balance = balance + $1
WHERE id = $2
`, amount, toID)
if err != nil {
return fmt.Errorf("add to destination: %w", err)
}
// Commit transaction
if err := tx.Commit(); err != nil {
return fmt.Errorf("commit transaction: %w", err)
}
return nil
}func updatePrices(ctx context.Context, db *sql.DB, updates []PriceUpdate) error {
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer tx.Rollback()
stmt, err := tx.PrepareContext(ctx, `
UPDATE products SET price = $1 WHERE id = $2
`)
if err != nil {
return err
}
defer stmt.Close()
for _, u := range updates {
_, err := stmt.ExecContext(ctx, u.Price, u.ID)
if err != nil {
return err
}
}
return tx.Commit()
}func batchInsert(db *sql.DB, products []Product) error {
stmt, err := db.Prepare(`
INSERT INTO products (name, price, discount)
VALUES ($1, $2, $3)
`)
if err != nil {
return err
}
defer stmt.Close()
for _, p := range products {
_, err := stmt.Exec(p.Name, p.Price, p.Discount)
if err != nil {
return fmt.Errorf("insert product %s: %w", p.Name, err)
}
}
return nil
}func getTotalRevenue(db *sql.DB) (decimal.Decimal, error) {
var total decimal.NullDecimal // May be NULL if no rows
err := db.QueryRow(`
SELECT SUM(price * quantity) FROM orders
`).Scan(&total)
if err != nil {
return decimal.Zero, err
}
if !total.Valid {
return decimal.Zero, nil // No orders
}
return total.Decimal, nil
}
func getAveragePrice(db *sql.DB) (decimal.Decimal, error) {
var avg decimal.NullDecimal
err := db.QueryRow(`
SELECT AVG(price) FROM products
`).Scan(&avg)
if err != nil {
return decimal.Zero, err
}
if !avg.Valid {
return decimal.Zero, nil // No products
}
return avg.Decimal, nil
}type PriceHistory struct {
ProductID int
Price decimal.Decimal
ChangedAt time.Time
}
func recordPriceChange(db *sql.DB, productID int, newPrice decimal.Decimal) error {
// Insert into history
_, err := db.Exec(`
INSERT INTO price_history (product_id, price, changed_at)
VALUES ($1, $2, NOW())
`, productID, newPrice)
if err != nil {
return err
}
// Update current price
_, err = db.Exec(`
UPDATE products SET price = $1 WHERE id = $2
`, newPrice, productID)
return err
}type SalesReport struct {
TotalSales decimal.Decimal
TotalDiscount decimal.Decimal
NetRevenue decimal.Decimal
AvgOrderValue decimal.Decimal
}
func generateSalesReport(db *sql.DB, startDate, endDate time.Time) (*SalesReport, error) {
var report SalesReport
err := db.QueryRow(`
SELECT
COALESCE(SUM(total), 0) as total_sales,
COALESCE(SUM(discount), 0) as total_discount,
COALESCE(SUM(total - discount), 0) as net_revenue,
COALESCE(AVG(total), 0) as avg_order_value
FROM orders
WHERE created_at BETWEEN $1 AND $2
`, startDate, endDate).Scan(
&report.TotalSales,
&report.TotalDiscount,
&report.NetRevenue,
&report.AvgOrderValue,
)
if err != nil {
return nil, err
}
return &report, nil
}func getInventoryValue(db *sql.DB) (decimal.Decimal, error) {
var total decimal.Decimal
rows, err := db.Query(`
SELECT cost, quantity FROM inventory
`)
if err != nil {
return decimal.Zero, err
}
defer rows.Close()
total = decimal.Zero
for rows.Next() {
var cost decimal.Decimal
var quantity int64
if err := rows.Scan(&cost, &quantity); err != nil {
return decimal.Zero, err
}
itemValue := cost.Mul(decimal.NewFromInt(quantity))
total = total.Add(itemValue)
}
return total, rows.Err()
}// Works well with NUMERIC/DECIMAL types
// Stored as string via driver
// Example schema
CREATE TABLE products (
id SERIAL PRIMARY KEY,
price NUMERIC(10, 2) NOT NULL,
cost NUMERIC(10, 4)
);
// Usage is identical to examples above// Works with DECIMAL type
// Note: MySQL driver may return different types
// Example schema
CREATE TABLE products (
id INT AUTO_INCREMENT PRIMARY KEY,
price DECIMAL(10, 2) NOT NULL,
cost DECIMAL(10, 4)
);
// Scan handles multiple types automatically// Best stored as TEXT for precision
// Can also use REAL but may lose precision
// Example schema
CREATE TABLE products (
id INTEGER PRIMARY KEY AUTOINCREMENT,
price TEXT NOT NULL,
cost TEXT
);
// Usage is identicalvar price decimal.Decimal
err := db.QueryRow("SELECT price FROM products WHERE id = $1", id).
Scan(&price)
if err == sql.ErrNoRows {
return fmt.Errorf("product not found")
}
if err != nil {
return fmt.Errorf("scan price: %w", err)
}// If database contains invalid decimal string
var price decimal.Decimal
err := db.QueryRow("SELECT price FROM products WHERE id = $1", id).
Scan(&price)
// Scan will return error if value cannot be parsed
if err != nil {
return fmt.Errorf("invalid price in database: %w", err)
}Use prepared statements for batch operations:
stmt, _ := db.Prepare("INSERT INTO products (price) VALUES ($1)")
defer stmt.Close()
for _, price := range prices {
stmt.Exec(price)
}Do calculations in Go, not SQL:
// GOOD: Calculate in Go with full precision
var prices []decimal.Decimal
// ... query prices
total := decimal.Zero
for _, p := range prices {
total = total.Add(p)
}
// AVOID: Database calculations may lose precision
db.QueryRow("SELECT SUM(price) FROM products")Batch queries when possible:
// Query multiple rows at once
rows, _ := db.Query("SELECT price FROM products WHERE id = ANY($1)", ids)Install with Tessl CLI
npx tessl i tessl/golang-github-com-shopspring--decimal@1.4.1