db.begin()返回的tx必须显式commit()或rollback(),否则连接与锁长期占用;defer仅用于panic兜底回滚,不可用于自动提交;tx非并发安全,须单goroutine内完成全部操作,且钩子中所有db操作须显式使用tx而非db。

db.Begin() 返回的事务对象必须在业务逻辑结束前显式 Commit() 或 Rollback(),否则连接会一直占用、锁不释放,最终拖垮数据库。
事务不能靠 defer 自动提交或回滚
很多人误以为写个 defer tx.Commit() 就能“自动收尾”,这是危险的——它会在函数退出时无条件执行,哪怕前面已经出错并调用了 tx.Rollback(),最后仍会触发 Commit(),造成数据错乱。
- 正确做法是:只用
defer做兜底回滚(比如 panic 场景),但主流程中必须手动判断错误后显式Rollback(),成功后再Commit() - 别把
tx.Commit()放在defer里;也别把tx.Rollback()写两次(一次显式 + 一次 defer) - 如果用了
context,优先选db.BeginTx(ctx, opts),超时会自动中断事务,避免悬挂
事务对象不能跨 goroutine 传递
*sql.Tx 不是并发安全的,它的内部状态(如连接绑定、语句缓存)只服务于单个 goroutine。一旦你把它塞进 channel、传给另一个 goroutine 执行 Exec(),大概率触发 sql: transaction has already been committed or rolled back 或更隐蔽的竞态行为。
- 所有基于该
tx的操作(Query、Exec、Prepare)必须在同一个 goroutine 内完成 - 不要试图“复用”一个
tx处理多个并发请求;每个请求应有自己的事务生命周期 - 若需异步写日志或发通知,务必在
Commit()后再启动 goroutine,且不能依赖tx对象
GORM 中事务生命周期容易被 hook 干扰
GORM 的钩子(如 BeforeCreate、AfterSave)默认运行在当前事务上下文中——这本是优点,但若你在钩子里又调用了 db.Create() 等未指定事务的方法,就会意外脱离当前 tx,走默认连接池,导致部分操作不在事务内。
- 钩子函数内所有数据库操作,必须显式传入当前事务:
tx.Create(&u)而非db.Create(&u) - 避免在钩子里调用其他 service 方法,除非你确认它们接收并透传了
*gorm.DB实例 - 开启
logger.Info级别日志,观察 SQL 是否都带SELECT ... FOR UPDATE或出现在同一事务 ID 下,可快速验证是否“掉事务”
PostgreSQL/MySQL 驱动对事务结束的实际影响不同
MySQL 在 tx.Commit() 成功后立即释放行锁;而 PostgreSQL 必须等整个事务会话关闭(即 tx 对象被 GC 或显式 Close)才真正清理快照和锁资源——这意味着即使你 Commit() 了,若没让 tx 变量及时不可达,连接可能长期挂起。
- Go 1.22+ 推荐在
Commit()/Rollback()后加一行tx = nil,协助 GC 尽快回收 - 使用
pgx/v5时,tx.Close()是必需调用,不是可选;标准database/sql则不用,但也不能长期持有tx引用 - 监控
pg_stat_activity或information_schema.INNODB_TRX,看是否存在trx_state = 'COMMITTED'却长时间未消失的记录,就是生命周期管理失效的信号
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











