Around Go: SQL services · 3 MIN READ
SQL transactions: every participating call uses the transaction
Place an atomic business transition inside one explicit transaction owner.
Beginning a transaction does not attach later calls on sql.DB to it. The transaction handle owns a particular transactional connection. A helper that accidentally queries db instead of tx may see another state, escape rollback or wait for a second connection while holding the first.
SQL transactions: every participating call uses the transaction · A MAP OF THE IDEA
- Validate the command
- Begin one transaction
- Use tx for every call
- Commit or roll back
Useful when
- Place an atomic business transition inside one explicit transaction owner.
- One owner starts the transaction, passes its handle to every participating call and finishes it. Database atomicity does not include remote side effects.
Watch out
- A transaction does not automatically include pool calls or an external provider. Check commit outcomes and idempotency at the actual boundary.
Implementation & operation cost: Choose the policy from the actual contract, then verify its failure path in the target environment.
Make the atomic boundary visible
This abbreviated PostgreSQL transfer updates both accounts through tx. Reject nonpositive amounts before starting, use an exact integer minor-unit representation and treat account currency and authorization as additional domain constraints.
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer tx.Rollback()
result, err := tx.ExecContext(ctx,
`UPDATE accounts SET balance = balance - $1 WHERE id = $2 AND balance >= $1`, amount, from)
if err != nil {
return err
}
n, err := result.RowsAffected()
if err != nil {
return err
}
if n != 1 {
return ErrInsufficientOrMissing
}
result, err = tx.ExecContext(ctx,
`UPDATE accounts SET balance = balance + $1 WHERE id = $2`, amount, to)
if err != nil {
return err
}
n, err = result.RowsAffected()
if err != nil {
return err
}
if n != 1 {
return ErrDestinationMissing
}
return tx.Commit()This function-body fragment assumes validated amount/from/to and application errors. Every write uses tx. The deferred Rollback is harmless after a successful Commit. A complete transfer also scopes ownership, checks currency and overflow bounds, and defines retry/idempotency semantics.
Pass a narrow transaction capability
A helper can accept the ExecContext or QueryContext capability it needs instead of a hidden global db. That makes accidental pool calls easier to see. The runtime guarantee still comes from passing the same transaction handle to every participating operation, not from an interface name alone.
Keep external effects outside the callback
An email, HTTP payment or message publish is not rolled back by the SQL transaction. Persist a durable outbox or intent with the state change, then deliver through an idempotent protocol. Do not keep a transaction open while waiting for a user or a slow remote service.
Do not interpret every commit error as an abort
A lost reply can make a durable outcome uncertain. Distinguish documented serialization/deadlock failures from transport ambiguity. Use a stable operation ID to reconcile the latter. Test abort-before-write, error-after-write and commit-then-disconnect separately.
TAKE THIS WITH YOU
One owner starts the transaction, passes its handle to every participating call and finishes it. Database atomicity does not include remote side effects.
Check your understanding
A helper calls db.ExecContext after its caller opened tx. Is that write guaranteed to roll back with tx?
Go to the source
Progress stays in this browser. No account needed.