Transacciones, Sentencias Preparadas y sql.Tx
Las transacciones agrupan múltiples sentencias en una única unidad atómica.
Busca en todas las páginas de la documentación
Las transacciones agrupan múltiples sentencias en una única unidad atómica.
Las sentencias preparadas reutilizan SQL analizado para parámetros repetidos.
sql.Tx une ambos: comienza con contexto, ejecuta dentro del límite, luego confirma o revierte.
BeginTx(ctx, opts) inicia una transacción con nivel de aislamiento opcional.
Commit persiste los cambios; Rollback los descarta - llama a una ruta después de errores.
PrepareContext crea un Stmt; Tx.Stmt lo reutiliza dentro de una transacción.
Delega Rollback después de BeginTx para que las rutas de error no filtren transacciones abiertas.
Tarjeta de receta de referencia rápida - lista para copiar y pegar.
func Transfer(ctx context.Context, db *sql.DB, from, to int64, amount int) error {
tx, err := db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelReadCommitted})
if err != nil {
return err
}
defer tx.Rollback()
if _, err := tx.ExecContext(ctx,
`UPDATE accounts SET balance = balance - $1 WHERE id = $2`, amount, from,
); err != nil {
return err
}
if _, err := tx.ExecContext(ctx,
`UPDATE accounts SET balance = balance + $1 WHERE id = $2`, amount, to,
); err != nil {
return err
}
return tx.Commit()
}Cuándo usar esto:
package main
import (
"context"
"database/sql"
"fmt"
_ "github.com/mattn/go-sqlite3"
)
func main() {
db, _ := sql.Open("sqlite3", ":memory:")
defer db.Close()
ctx := context.Background()
_, _ = db.ExecContext(ctx, `CREATE TABLE ledger (
id INTEGER PRIMARY KEY,
balance INTEGER NOT NULL
)`)
_, _ = db.ExecContext(ctx, `INSERT INTO ledger (id, balance) VALUES (1, 100), (2, 0)`)
stmt, err := db.PrepareContext(ctx, `UPDATE ledger SET balance = balance + ? WHERE id = ?`)
if err != nil {
panic(err)
}
defer stmt.Close()
tx, err := db.BeginTx(ctx, nil)
if err != nil {
panic(err)
}
defer tx.Rollback()
txStmt := tx.Stmt(stmt)
if _, err := txStmt.ExecContext(ctx, -25, 1); err != nil {
panic(err)
}
if _, err := txStmt.ExecContext(ctx, 25, 2); err != nil {
panic(err)
}
fmt.Println(tx.Commit())
}Lo que esto demuestra:
PrepareContext una vez, Tx.Stmt para vincular la sentencia a la transacción activa.defer tx.Rollback() cubre las salidas por pánico y error; Commit tiene éxito después de que el trabajo se completa.ctx fluye a través de begin, exec y commit para la cancelación.database/sql.BeginTx extrae una conexión dedicada hasta que se confirma o se revierte.tx se ejecutan en esa misma conexión, preservando las garantías de aislamiento.Stmt pueden almacenar en caché planes preparados del lado del servidor dependiendo de la configuración del driver.Commit, el objeto de transacción queda inválido; inicia un nuevo BeginTx para más trabajo.| Nivel | Constante | Uso Típico |
|---|---|---|
| Predeterminado | nil opts | Predeterminado del driver (a menudo read committed) |
| Read committed | LevelReadCommitted | La mayoría de los servicios OLTP |
| Repeatable read | LevelRepeatableRead | Lecturas consistentes en una tx |
| Serializable | LevelSerializable | El más fuerte, puede reintentar en caso de conflicto |
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer func() {
_ = tx.Rollback() // no-op después de un Commit exitoso
}()Rollback después de Commit devuelve sql.ErrTxDone - seguro de ignorar en defer.sql.ErrTxDone solo si confirmas dos veces por error.Algunos equipos usan SQL SAVEPOINT crudo dentro de una tx para reversiones parciales.
Prefiere menos transacciones, más cortas, sobre anidamiento profundo de puntos de guardado a menos que el equipo de la base de datos documente patrones.
Rollback en la ruta de error - La conexión permanece en estado abortado y se filtra del pool. Solución: defer tx.Rollback() inmediatamente después de BeginTx.db.Exec dentro de una tx abierta para escrituras relacionadas - Las sentencias se ejecutan en conexiones diferentes y rompen la atomicidad. Solución: solo tx.ExecContext para trabajo transaccional.stmt.Exec mientras una tx está abierta omite la conexión de la transacción. Solución: tx.Stmt(stmt) o preparar directamente en tx.Stmt mientras la tx aún se está ejecutando - Comportamiento indefinido en algunos drivers. Solución: cierra las sentencias después de que la tx se complete o usa preparaciones de corta duración por solicitud.| Alternativa | Usar Cuando | No Usar Cuando |
|---|---|---|
Exec de una sola sentencia | UPDATE atómico | Invariantes multi-tabla |
| Saga / patrón outbox | Consistencia entre servicios | El ACID de una sola base de datos es suficiente |
| Ayudante de transacción ORM | Equipo en wrappers GORM/sqlx | Necesitas documentación explícita de aislamiento |
| Bloqueos consultivos (Advisory locks) | Mutex a nivel de aplicación en la DB | Las simples actualizaciones de filas son suficientes |
SELECT FOR UPDATE | Bloqueo pesimista de fila | Alta contención sin tx cortas |
Sí, inmediatamente después de BeginTx.
Un Commit exitoso hace que el Rollback delegado sea una operación nula (no-op).
Expón ayudantes WithTx(ctx, fn) o acepta *sql.Tx en métodos internos.
Mantén *sql.DB como la dependencia constructora predeterminada.
BeginTx y ExecContext respetan el contexto; revierte ante la cancelación para liberar la conexión.
No dejes las tx abiertas después de la salida del manejador.
Cuando el mismo SQL se ejecuta muchas veces por segundo y el perfilado muestra sobrecarga de análisis.
Muchos drivers almacenan en caché automáticamente; compara primero.
Algunas bases de datos optimizan las tx de solo lectura; usa TxOptions{ReadOnly: true} cuando el driver lo soporte para consultas de informes.
Fuerza el fallo de la segunda sentencia y verifica que el primer cambio no sea visible después de Rollback.
Usa SQLite o Postgres reales en pruebas de integración.
Comparte una tx solo cuando el driver y el nivel de aislamiento permitan el uso concurrente - generalmente una tx por goroutine es más seguro.
Serializa escrituras en una única conexión de tx.
Algunos DDL no se pueden ejecutar dentro de txs en ciertos motores; las herramientas de migración documentan el comportamiento por base de datos.
Los servicios en línea no deben ejecutar migraciones por solicitud.
sql.ErrTxDone - indica un error lógico; corrige las rutas de llamada en lugar de tragarlas silenciosamente.
Una tx abierta retiene una conexión del pool hasta que se confirma o se revierte.
Mantén las tx cortas para evitar la inanición del pool bajo carga.
Versiones de Stack: Esta página fue escrita para Go 1.26.x (predeterminado GC de Green Tea, go fix modernizers - verifica el parche en la compilación), chi (última - verifica en la compilación), gin (última - verifica en la compilación), echo (última - verifica en la compilación), google.golang.org/grpc (última - verifica en la compilación), sigs.k8s.io/controller-runtime (última - verifica en la compilación), kubebuilder (última - verifica en la compilación), tinygo (última - verifica los objetivos de placa en la compilación), wazero (última - verifica en la compilación) y golangci-lint (última - verifica el conjunto de linters en la compilación).
Revisado por Chris St. John·Última actualización: 18 jul 2026