Files

1.5 KiB

Transactions, Isolation Levels, and Locking

Basic transaction pattern

tx, err := db.BeginTxx(ctx, nil) // default isolation (READ COMMITTED)
if err != nil {
    return fmt.Errorf("beginning transaction: %w", err)
}
defer tx.Rollback() // no-op if already committed

// ... execute queries using tx ...

if err := tx.Commit(); err != nil {
    return fmt.Errorf("committing transaction: %w", err)
}

Custom isolation level

tx, err := db.BeginTxx(ctx, &sql.TxOptions{
    Isolation: sql.LevelSerializable, // strongest guarantee
})
Level Use when
LevelReadCommitted Default — good for most operations
LevelRepeatableRead Need consistent reads within a transaction
LevelSerializable Financial operations, inventory, anything with strict consistency

SELECT FOR UPDATE — prevent race conditions

var balance int
err := tx.GetContext(ctx, &balance, "SELECT balance FROM accounts WHERE id = $1 FOR UPDATE", accountID)
// Row is locked until tx.Commit() or tx.Rollback()

Use FOR UPDATE when you read a value, compute something from it, and then write it back. Without the lock, concurrent transactions can read stale data.

Locking variants

Clause Effect
FOR UPDATE Locks rows for write — other transactions block on same rows
FOR UPDATE NOWAIT Same, but fails immediately instead of waiting
FOR SHARE Locks rows for read — prevents writes but allows other reads