Go中db.Exec失败后事务不会自动回滚,必须显式调用tx.Rollback();常见错误是return err后未回滚,正确做法是defer tx.Rollback()并仅在无错时commit。

Go 里 db.Exec 失败后事务没回滚?先确认你是不是手动调用了 tx.Commit()
Go 的 sql.Tx 不会自动回滚,哪怕函数 panic 或返回 error,只要没显式调用 tx.Rollback(),连接就一直挂着,下次 tx.Commit() 还可能成功——但数据已脏。常见错误是写成这样:
func updateUser(tx *sql.Tx, id int, name string) error {
_, err := tx.Exec("UPDATE users SET name = ? WHERE id = ?", name, id)
if err != nil {
return err // ❌ 这里 return 了,但 tx 没 rollback
}
return tx.Commit() // ✅ 只有这行跑到了才 commit
}
真正安全的做法是:用 defer 绑定回滚,再靠 if err != nil 控制是否跳过 commit:
- 所有数据库操作必须在同一个
*sql.Tx上执行,不能混用db.Exec和tx.Exec -
tx.Rollback()可以被多次调用,不会报错;但tx.Commit()成功后再次调用会返回"sql: transaction has already been committed or rolled back" - 如果事务中调用了外部 HTTP 或文件操作,它们失败时也得触发
tx.Rollback(),否则数据库状态和业务逻辑脱节
用 tx.QueryRow 查不到数据时,err == sql.ErrNoRows 是正常分支,不是错误
很多人把 sql.ErrNoRows 当成异常处理,结果一查无记录就回滚整个事务,导致本该成功的 insert/update 被误丢弃。它只是说明查询没命中,不表示事务出问题。
-
sql.ErrNoRows是一个预定义的error值,类型是*sql.ErrNoRows,可用errors.Is(err, sql.ErrNoRows)判断 - 如果你的业务逻辑允许“查无此用户,那就新建”,那这里就不该 return error,更不该 rollback
- 注意:只有
QueryRow().Scan()会返回sql.ErrNoRows;Query()返回*sql.Rows,需要自己检查rows.Next()
嵌套函数里传 *sql.Tx 还是 interface{ Exec(...), Query(...) }?选后者更易测试
直接传 *sql.Tx 看似简单,但会让单元测试必须启动真实数据库或 mock 整个 sql.Tx 方法集。实际项目中,更推荐抽象一层:
立即学习“go语言免费学习笔记(深入)”;
type DBExecutor interface {
Exec(query string, args ...interface{}) (sql.Result, error)
Query(query string, args ...interface{}) (*sql.Rows, error)
QueryRow(query string, args ...interface{}) *sql.Row
}
这样你可以:
- 在 handler 层传入
tx(它实现了DBExecutor),测试时传入轻量 mock 实现 - 避免跨包强依赖
"database/sql",降低耦合 - 万一将来换 ORM(比如
ent或gorm),只要新 client 包装出同名方法,上层逻辑几乎不用动
context.Context 超时没中断事务?因为 sql.Tx 默认不响应 cancel
db.BeginTx(ctx, nil) 中的 ctx 只控制“开启事务”这一步是否超时,一旦事务建立成功,后续所有 tx.Exec、tx.Query 都不再受该 ctx 约束——即使 ctx 已 cancel,SQL 还在跑。
- 若需全程控制,必须用
tx.StmtContext(ctx, stmt)或tx.QueryRowContext(ctx, ...)等带 Context 的方法 - 注意:
tx.QueryRow(...).Scan()是两步:执行 + 扫描,前者可被 ctx 中断,后者不行;所以要确保Scan()前 ctx 仍有效 - PostgreSQL 用户额外注意:
pgx的Conn.BeginTx()支持自动清理,但标准库database/sql不支持,别指望 ctx cancel 后连接自动 close
事务的边界比看起来 tight 得多:rollback 要手动、ErrNoRows 不是错误、接口抽象影响可测性、context 只管开头不管结尾——漏掉任意一点,都可能让错误静默蔓延到下游服务。










