database/sql 事务错误回滚与提交判断的清理清单
来源:17golang原创
时间:2026-11-12 18:12:59 209浏览 收藏
线上写订单、库存或账务数据时,事务代码最容易漏掉的不是 SQL 本身,而是错误路径:开启失败没有继续判断、执行失败没有回滚、Commit 返回错误却被当成“肯定没写进去”。一份可靠的清理清单应该把这三种状态分开处理:事务尚未开始、事务明确失败、提交结果未知。
- 拿到
*sql.Tx后立即登记defer tx.Rollback(),让未提交路径自动收口。 - 业务操作失败先保留原始错误;回滚失败作为附加信息记录,不要覆盖根因。
Commit失败不等于数据库一定没有变化,涉及重试时要先确认幂等键和结果查询方案。
先把事务状态和清理责任分开
database/sql 的事务必须以 Commit 或 Rollback 结束。开启阶段的错误不属于“回滚失败”,因为此时通常还没有可供调用的事务对象;进入事务后的错误才需要走回滚路径。用 BeginTx 接收请求上下文,能让取消信号参与事务生命周期,但也意味着上下文取消后不能再把提交结果想当然地当成成功或失败。

实际代码可以把兜底回滚放在拿到事务之后。提交成功后,后续的 Rollback 会返回事务已结束一类的错误,因此不要把这个 defer 当成第二次提交;它只是覆盖提前返回和异常分支。
// saveOrder 只展示事务边界;具体驱动和表结构由调用方提供。
func saveOrder(ctx context.Context, db *sql.DB, orderID int64) error {
tx, err := db.BeginTx(ctx, nil)
if err != nil {
// 开启失败时没有可清理的 Tx,直接返回原始错误。
return fmt.Errorf("begin transaction: %w", err)
}
defer tx.Rollback() // 未提交路径自动回滚;提交后的 ErrTxDone 不改变成功结果。
if _, err = tx.ExecContext(ctx,
"UPDATE orders SET status = ? WHERE id = ?", "paid", orderID); err != nil {
// 业务错误优先保留,回滚只负责释放事务状态。
return fmt.Errorf("update order: %w", err)
}
if err = tx.Commit(); err != nil {
// Commit 失败可能处于未知状态,不能直接当作“未提交”重放写入。
return fmt.Errorf("commit transaction: %w", err)
}
return nil
}
执行失败时,回滚错误怎么处理
执行语句、扫描结果或业务校验失败,都应该先让 defer 触发回滚,再把原始错误返回。若需要显式记录回滚结果,可以把回滚错误作为日志字段或包装错误的附加信息;不要用“回滚失败”覆盖真正导致订单更新失败的 SQL 错误。只有在回滚本身影响后续补偿判断时,才把两者一起上报。
查询类事务还要把 Rows.Close 和 Rows.Err 纳入检查。行迭代中途发生网络或驱动错误,不能因为已经读到几行数据就继续提交。资源清理和事务清理是两条线:关闭 rows 释放结果集,回滚或提交结束事务。
| 位置 | 需要判断 | 处理动作 |
|---|---|---|
BeginTx | 返回的 error | 直接返回,不调用 tx 方法 |
| Exec/Scan/Rows.Err | 业务或驱动错误 | 保留原错误,走回滚兜底 |
Rollback | 回滚是否也失败 | 记录附加错误,标记需要人工或补偿处理 |
Commit | 提交返回值 | 失败按未知状态处理,避免无条件重试 |

Commit 失败不要直接按失败重放
Commit 返回错误的危险点在于:调用方可能已经把请求发给数据库,但客户端没有可靠收到最终结果。此时最稳妥的清理清单是记录事务标识、业务幂等键、数据库错误和请求上下文状态,再根据业务查询确认最终状态。支付、库存扣减这类不可重复写入,应该让重试依赖幂等键或补偿任务;普通的网络错误不能自动推出“数据库没有变化”。
如果使用 BeginTx 的 context 被取消,标准库会尝试回滚,提交也可能返回错误。代码层面仍要把这个结果交给上层,而不是在 defer 中偷偷重试。重试策略属于业务协议,必须知道重复执行会不会产生第二条订单、第二次扣减或状态覆盖。
发布前的事务清理清单
- 开启失败是否立即返回,且没有对 nil 的
tx调用方法? - 拿到事务后是否立刻登记回滚兜底?
- 每个写操作、扫描操作和
Rows.Err是否都检查? - 回滚失败是否作为附加证据记录,而不是丢掉原始错误?
- 提交失败是否按未知状态进入幂等查询或补偿流程?
- 日志里是否有业务幂等键、事务阶段和驱动错误,且没有输出敏感参数?
相关问题
defer tx.Rollback 会不会影响成功提交?
正常提交后事务已经结束,defer 中的回滚只会得到事务完成类错误;它不会撤销已经成功提交的结果。
Commit 报错后能不能马上再调用 Rollback?
可以把回滚作为尽力清理,但不能据此判断写入一定未发生。真正的重试前,先用幂等键或业务查询确认状态。
为什么不只在错误分支手写 Rollback?
分支越多越容易遗漏。defer 提供统一兜底,显式回滚只在需要组合错误信息或提前结束时使用。
官方资料:https://pkg.go.dev/database/sql;事务操作说明:https://go.dev/doc/database/execute-transactions。这些页面可用于核对 Tx 的结束语义和 BeginTx 的上下文行为。
-
116 收藏
-
170 收藏
-
477 收藏
-
419 收藏
-
408 收藏
-
430 收藏
-
136 收藏
-
463 收藏
-
Golang · Go教程 | 27分钟前 | Go教程 · database/sql · database/sql Go数据库 sql.Stmt PrepareContext Conn.PrepareContext123 收藏
-
292 收藏
-
281 收藏
-
304 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习