go事务需手动控制回滚:必须用committed标志位配合defer tx.rollback(),commit成功后置true;rollback失败只需记录日志不可抛错;查无结果须区分queryrow.scan与query+next;事务必须由db.begin()开启而非手拼begin。

不调用 tx.Rollback() 就不会回滚,哪怕函数 panic 或提前 return —— 这是 Go 事务最常踩的坑。
为什么 defer tx.Rollback() 必须配标志位
直接写 defer tx.Rollback() 看似安全,但一旦 tx.Commit() 成功,再执行它会返回 sql.ErrTxDone,这不是 bug,而是事务已终结的信号。如果日志里频繁看到 sql: transaction has already been committed or rolled back,大概率是没做状态控制。
- 必须用布尔变量(如
committed := false)标记是否已提交 -
defer里只在!committed时调用tx.Rollback() -
tx.Commit()成功后立即设committed = true,避免 defer 误触发 - 别在
if err != nil分支里单独写tx.Rollback()后return—— defer 已覆盖,重复调用反而出错
tx.Rollback() 返回 error 怎么办
tx.Rollback() 出错 ≠ 回滚失败,而是“无法确认结果”。比如连接已断,请求发不出去,数据库侧很可能已完成回滚,但 Go 拿不到响应。
- 必须检查 error,但不能
return err向上抛 —— 调用方会误判业务异常 - 只记录日志:
if err := tx.Rollback(); err != nil { log.Printf("rollback failed: %v", err) } - 绝不能在
tx.Rollback()后继续调tx.Query()等方法,此时tx已失效,必 panic - 用
errors.Is(err, sql.ErrTxDone)区分“已终结”和真正网络错误,前者可忽略
事务里查不到数据,别乱用 sql.ErrNoRows
sql.ErrNoRows 只在 tx.QueryRow().Scan() 查无结果时返回;而 tx.Query() + rows.Next() 遇到空集只是 Next() 返回 false,err 仍是 nil。混用会导致逻辑跳过或误报。
- 要“必须且仅有一行”,用
QueryRow().Scan(),并用errors.Is(err, sql.ErrNoRows)判断 - 要“零或多行”,用
Query()+for rows.Next(),靠Next()返回值控制循环 - 别用
err == sql.ErrNoRows,Go 1.13+ 必须用errors.Is(),否则驱动自定义错误类型会漏判 - 事务中任何操作失败(包括 Scan 失败),都该中断流程,而不是继续往下走
测试时回滚没生效?先看是不是用了 db.Exec("BEGIN")
手拼 "BEGIN" 字符串执行,只是发了一条 SQL,根本没创建 *sql.Tx 对象。后续所有 db.Query() 都走默认连接池,事务形同虚设。
- 事务必须从
db.Begin()或db.BeginTx()开始,所有操作走tx对象 - 测试中务必用
defer tx.Rollback()开头,哪怕你打算 commit —— 先保底清理 - 验证回滚是否真实发生,要用另一个干净连接(如新
db.QueryRow())去查,不能依赖延迟观察 - SQLMock 测试需显式调
mock.ExpectBegin()和mock.ExpectRollback(),否则它根本监听不到事务方法调用
事务不是黑盒,它的生命周期完全由你代码控制。最容易被忽略的点是:Commit 失败后,tx 对象不可再用,但很多人还在错误处理分支里试图再次 Rollback() 或 Query() —— 此时错误信息往往掩盖了真正的源头。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











