事务必须传 context 以避免卡死,所有 SQL 操作须用 tx 对象而非 db,DAO 函数应接收 *sql.Tx,defer tx.Rollback() 需紧跟 BeginTx 并在 Commit 后置 nil,推荐使用 db.Transaction 闭包模式确保安全。

db.BeginTx() 必须传 context,否则事务会卡死
不带 context.Context 启动的事务没有超时能力。MySQL 的 innodb_lock_wait_timeout 默认 50 秒,但 Go 层完全感知不到——goroutine 挂起、连接池耗尽、API 响应无限等待,都是静默发生的。
正确做法是始终用 context.WithTimeout 包一层:
ctx, cancel := context.WithTimeout(c.Request().Context(), 5*time.Second)
defer cancel()
tx, err := db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelRepeatableRead})注意:c.Request().Context() 是 Echo 中最自然的上下文来源;若在非 HTTP 场景(如定时任务),可用 context.Background(),但必须预留 timeout 接口。
- MySQL 默认隔离级别是
REPEATABLE READ,但 Go 不显式传TxOptions就不会发SET TRANSACTION ISOLATION LEVEL,实际执行时仍走 MySQL 默认值(这点容易误以为“没设就是默认”,其实驱动层不干预) - PostgreSQL 更推荐
READ COMMITTED,SERIALIZABLE开销大,线上慎用
所有 SQL 操作必须走 tx 对象,混用 db 是静默数据撕裂
这是最常踩的坑:在事务函数里不小心调了 db.Query() 或 db.Exec(),那条语句根本不在事务中——它从连接池拿了新连接,执行完立刻提交,tx.Commit() 完全无法控制它。
立即学习“go语言免费学习笔记(深入)”;
现象是:tx.Commit() 返回 nil,但查库发现只有部分更新生效,状态不一致。
- DAO 函数签名必须接收
*sql.Tx,例如func updateUser(tx *sql.Tx, id int, name string) error - 所有操作只能用
tx.QueryRow()、tx.Exec()、tx.Prepare(),禁止出现任何db.Xxx() -
*sql.Tx不是线程安全的,别在go func() { tx.Exec() }()里跨 goroutine 复用
defer tx.Rollback() 要紧跟 BeginTx,且 Commit 后需置 nil
手动写 if err != nil { tx.Rollback() } 极易遗漏:panic 时跳过、提前 return、嵌套 error 分支漏覆盖、甚至忘记写。
标准姿势是三步紧挨着写:
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer tx.Rollback() // 紧跟 BeginTx,确保一定执行
// ... 执行业务逻辑
if err := tx.Commit(); err != nil {
return err
}
tx = nil // 阻止 defer rollback 执行这样既防 panic 跳过,也避免多次 commit/rollback 误调用。
用 db.Transaction 闭包可彻底规避手动管理
Go 标准库从较早版本就支持 db.Transaction() 闭包模式,它自动处理 Rollback/Commit 和上下文传递,比手写更安全。
示例:
err := db.Transaction(func(tx *sql.Tx) error {
_, err := tx.Exec("UPDATE accounts SET balance = balance - ? WHERE id = ?", amount, userID)
if err != nil {
return err // 自动触发 rollback
}
_, err = tx.Exec("INSERT INTO logs (...) VALUES (...)", ...)
return err // 成功则自动 commit
}, &sql.TxOptions{Isolation: sql.LevelRepeatableRead})这个模式强制你把全部逻辑塞进闭包,天然杜绝混用 db、漏 defer、忘置 nil 等问题。复杂事务建议优先选它。
真正难的不是写对语法,而是意识到:事务不是“加个 begin/commit 就行”,而是一整套上下文、连接、错误流、goroutine 边界都得对齐的约束体系。漏掉任意一环,数据一致性就可能在某个并发高峰悄悄崩塌。


















