go 的 sql.tx 不会自动回滚,必须用 defer func() { if err != nil { tx.rollback() } }() 保底;commit 后不可再 rollback;sql server 需显式设 autocommit=false;rollback 错误只需记录日志。

Go 的 sql.Tx 不会自动回滚,不调 tx.Rollback() 就不会回滚,哪怕函数 panic 或提前 return —— 这是绝大多数数据不一致问题的根源。
为什么 defer tx.Rollback() 必须放在 tx.Begin() 后立刻写
延迟执行的 tx.Rollback() 是保底机制:它确保无论后续代码怎么退出(error、panic、return),事务状态都不会悬而未决。但很多人把它写在错误分支里,或只在 if err != nil 时调用,结果 panic 时根本没走那条路。
- 必须写成
defer tx.Rollback(),且紧接在tx, err := db.Begin()之后 - 不要写成
if err != nil { tx.Rollback() }—— 它拦不住 panic - 不要在 defer 里直接调
tx.Rollback()而不做 error 判断,否则测试中常看到sql: transaction has already been committed or rolled back报错(这是预期行为,可忽略) - 正确姿势是:
defer func() { if err != nil { tx.Rollback() } }()或更稳妥的闭包捕获
tx.Commit() 失败后不能再调 tx.Rollback()
tx.Commit() 返回 error 并不意味着“回滚还没发生”,而是事务已终结 —— 此时 tx 对象进入 final 状态,任何后续调用(包括 tx.Rollback())都会返回 sql.ErrTxDone 或 panic。
- 一旦
err = tx.Commit()执行过,就不要再碰这个tx对象 - 别在 Commit 失败后补一句
tx.Rollback(),它无效且可能报错 - Commit 失败通常意味着网络中断或数据库崩溃,此时你唯一能做的就是记录日志,然后让上层决定是否重试整个业务流程
SQL Server 上回滚失效?检查 autocommit=false 是否生效
用 github.com/denisenkom/go-mssqldb 连 SQL Server 时,即使写了 tx.Rollback(),INSERT/UPDATE 仍被提交——大概率是连接启用了隐式 autocommit。
- 验证方式:在事务内执行
SELECT CASE WHEN @@OPTIONS & 2 = 2 THEN 'ON' ELSE 'OFF' END,返回ON即表示 autocommit 开启 - 修复方法:连接字符串中必须显式加上
autocommit=false,例如:server=localhost;database=test;user id=sa;password=xxx;autocommit=false - 注意:不是所有驱动都支持该参数,
go-mssqldb支持,但mssql(旧版)不支持;若用 ORM(如 gorp),需确认它是否透传该参数
Rollback() 自己出错怎么办
tx.Rollback() 返回 error 不代表“回滚失败”,而是“无法确认是否成功”——比如连接已断,发不出 rollback 请求。这时再重试或向上抛错反而破坏语义。
- 永远只记录日志:
if err := tx.Rollback(); err != nil { log.Printf("rollback failed: %v", err) } - 绝不能写
return err,这会让调用方误以为业务逻辑失败,实际数据早已回滚完毕 - Rollback() 后立即丢弃
tx引用,别试图在它上面再做任何事
最易被忽略的一点:database/sql 根本不支持 savepoint 和嵌套事务。所谓“部分回滚”,只能靠业务层拆成多个独立 db.Begin(),或者改用原生 SQL 的 SAVE TRANSACTION(SQL Server)或 SAVEPOINT(PostgreSQL)并手动管理 —— 但这已脱离 database/sql 的抽象层,需要你自己封装执行逻辑。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











