How do you handle transactions, NULLs and "no rows" correctly with database/sql?
Question 457MediumGo 1.22 to 1.25
Transactions pin a single connection; all statements must use the *sql.Tx, not db (using db inside a tx runs on another connection and can deadlock with MaxOpenConns=1).
No rows: QueryRow(...).Scan returns sql.ErrNoRows; check with errors.Is. NULLs: scanning NULL into a string errors — use sql.NullString, a pointer, or the generic sql.Null[T] (Go 1.22).
func transfer(ctx context.Context, db *sql.DB, from, to int64, amt int64) (err error) {
tx, err := db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelSerializable})
if err != nil {
return err
}
defer tx.Rollback() // no-op if committed
var bal int64
var note sql.Null[string]
err = tx.QueryRowContext(ctx,
"SELECT balance, note FROM accounts WHERE id = $1 FOR UPDATE", from).Scan(&bal, ¬e)
if errors.Is(err, sql.ErrNoRows) {
return fmt.Errorf("account %d not found", from)
} else if err != nil {
return err
}
if bal < amt {
return errors.New("insufficient funds")
}
if _, err = tx.ExecContext(ctx, "UPDATE accounts SET balance = balance - $1 WHERE id = $2", amt, from); err != nil {
return err
}
if _, err = tx.ExecContext(ctx, "UPDATE accounts SET balance = balance + $1 WHERE id = $2", amt, to); err != nil {
return err
}
return tx.Commit()
}
Always use placeholders (never fmt.Sprintf SQL) to prevent injection; placeholder syntax is driver-specific ($1 Postgres, ? MySQL).
More on Standard Library, HTTP & Systems Design in Go
- Q455What does this print? (nil vs empty slices and maps in JSON)
- Q456How does database/sql connection pooling work? Which settings matter and what causes connection leaks?
- Q458What is the exact io.Reader contract? What's wrong with this read loop?
- Q459Compose io primitives: upload a file to HTTP while computing its SHA-256 and limiting size, without buffering it in memory.
- Q460What are the pitfalls of bufio.Scanner and bufio.Writer?
- Q461Implement graceful shutdown for an HTTP server with background workers.