必须用 context.Context 启动事务以保障超时控制,所有 SQL 操作须通过 tx 对象执行,禁止混用 db;defer tx.Rollback() 应紧跟 BeginTx,Commit 后置 nil;优先使用 db.Transaction 闭包防错。

db.BeginTx() 必须传 context,否则超时无保障
不用 context.Context 的事务没有超时控制能力,一旦某条语句卡在锁等待或网络抖动里,goroutine 就会挂起,连接池可能被耗尽。Go 1.21+ 明确推荐统一用 db.BeginTx(ctx, opts),哪怕只是传 context.Background(),也为后续加 timeout 留出接口。
常见错误现象:tx.Exec("SELECT * FROM accounts WHERE id = ? FOR UPDATE", 123) 在 MySQL 上卡住 50 秒(默认 innodb_lock_wait_timeout),而 Go 层毫无感知,连接持续占用。
- 正确做法:用
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second),然后tx, err := db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelRepeatableRead}) - MySQL 默认隔离级别是
REPEATABLE READ,但 Go 层不显式设TxOptions就不会发SET TRANSACTION ISOLATION LEVEL语句 - PostgreSQL 对
READ COMMITTED支持最好,SERIALIZABLE开销大,线上慎用
所有 SQL 操作必须走 tx 对象,混用 db 是静默数据泄露
这是最常踩的坑:在事务里不小心调了 db.Query() 或 db.Exec(),那条语句根本不在事务上下文中——它从连接池拿了新连接,执行完立刻提交,tx.Commit() 完全感知不到。
现象是:tx.Commit() 返回 nil,但查库发现只有部分数据写入,状态撕裂。尤其容易发生在封装 DAO 方法后,忘了把 *sql.Tx 作为参数透传进去。
立即学习“go语言免费学习笔记(深入)”;
- 所有操作必须用
tx.QueryRow()、tx.Exec()、tx.Prepare(),不能出现任何db.Xxx() -
tx不是线程安全的,别跨 goroutine 复用(比如在go func() { tx.Exec() }()里用) - DAO 层函数签名得是
func updateUser(tx *sql.Tx, id int, name string) error,而不是func updateUser(db *sql.DB, ...)
defer tx.Rollback() 要紧跟 BeginTx,且 Commit 后需置 nil 阻止误执行
手动写 if err != nil { tx.Rollback() } 极易遗漏:panic 时跳过、提前 return、嵌套 error 分支没覆盖、甚至忘记写。正确姿势是 defer tx.Rollback() 紧跟 db.BeginTx() 后,再在 tx.Commit() 成功后把 tx 置为 nil 来阻止 rollback 执行。
注意:tx.Rollback() 返回 error 通常说明连接已断,此时再记日志也没用,忽略即可(除非你要做连接恢复);Go 1.21+ 提供 tx.RollbackUnlessCommitted() 更安全,但老版本得自己判 tx 是否为 nil。
- 错误示范:
if err != nil { tx.Rollback(); return err }—— panic 时不会执行 - 正确顺序:
tx, err := db.BeginTx(...); if err != nil { ... }; defer tx.Rollback(); ...; if err := tx.Commit(); err == nil { tx = nil } -
tx.Rollback()在连接已失效时可能返回 error,不必恐慌,也不必重试
GORM 的 db.Transaction 闭包不是语法糖,是防错刚需
绝大多数业务场景下,直接用 db.Transaction 闭包,而不是手写 db.Begin()。它自动处理 panic 回滚、成功提交、连接归还,避免人为遗漏 Commit() 或 Rollback() 导致连接卡死或数据部分落库。
常见错误现象:tx.Commit() 被跳过(比如分支 return 前没写),后续请求卡在 acquire connection timeout;或者闭包里误调 db.Create()(走的是普通连接),那条数据已不可逆写入,事务原子性彻底失效。
- ✅ 正确:所有 DB 操作都用闭包参数传入的
tx *gorm.DB,例如tx.Create(&order) - ❌ 危险:混用全局
db实例,哪怕只有一处,整个事务就失去原子性 - ⚠️ 注意:
db.Transaction不捕获逻辑错误——只有闭包返回非nilerror 才回滚;分支里忘了return err就会静默丢弃事务
事务体里放 HTTP 请求、Redis 写缓存、发 MQ 消息,轻则拖慢事务,重则耗尽连接池。真正安全的出口只有 tx.AfterCommit(),但它只对当前 tx 生效,且必须在闭包内注册。


















