Go cookbook · Intermediate
Run database transactions with database/sql
Multi-step writes leave partial data when one step fails, and connections leak.
Approach
Wrap transactional work in a helper that begins a transaction, defers a rollback (a no-op after commit), and
commits at the end. Use context-aware methods, FOR UPDATE row locks, and configure the pool.
Solution
package store
import (
"context"
"database/sql"
"errors"
"fmt"
"time"
)
var ErrInsufficientFunds = errors.New("insufficient funds")
func Open(dsn string) (*sql.DB, error) {
db, err := sql.Open("pgx", dsn) // driver registered by importing github.com/jackc/pgx/v5/stdlib
if err != nil {
return nil, err
}
db.SetMaxOpenConns(20)
db.SetMaxIdleConns(20)
db.SetConnMaxLifetime(30 * time.Minute)
return db, nil
}
func WithTx(ctx context.Context, db *sql.DB, fn func(*sql.Tx) error) error {
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer tx.Rollback() // returns sql.ErrTxDone after a successful commit; safe to ignore
if err := fn(tx); err != nil {
return err
}
return tx.Commit()
}
func Transfer(ctx context.Context, db *sql.DB, from, to int64, cents int64) error {
return WithTx(ctx, db, func(tx *sql.Tx) error {
var balance int64
err := tx.QueryRowContext(ctx, `SELECT balance FROM accounts WHERE id = $1 FOR UPDATE`, from).Scan(&balance)
if err != nil {
return fmt.Errorf("load account %d: %w", from, err)
}
if balance < cents {
return ErrInsufficientFunds
}
if _, err := tx.ExecContext(ctx, `UPDATE accounts SET balance = balance - $1 WHERE id = $2`, cents, from); err != nil {
return err
}
_, err = tx.ExecContext(ctx, `UPDATE accounts SET balance = balance + $1 WHERE id = $2`, cents, to)
return err
})
}
Pitfalls
- Not closing
*sql.Rows(usedefer rows.Close()and checkrows.Err()), exhausting the pool. - Using
dbinstead oftxinside the transaction function, running statements outside it. - Building SQL with
fmt.Sprintfand user input; always use placeholders.
Variations
- Lock rows in a consistent order (lower ID first) to avoid deadlocks in transfers.
sqlcgenerates type-safe Go code from SQL queries;pgxnative API is faster for PostgreSQL.
Further reading: database/sql transactions
Reference: https://pkg.go.dev/database/sql#DB.BeginTx
More Go recipes
- Benchmark code with testing.B.Loop
- Build a small, secure container image for a Go service
- Decode and validate JSON request bodies safely
- Embed static files and templates in the binary
- Load and validate configuration from environment variables
- Log structured JSON with slog and redact secrets
- Process jobs concurrently with a bounded worker pool
- Profile a running service with pprof