Go database/sql 事务提交后再 Rollback 为什么会返回错误
来源:17golang原创
时间:2026-11-05 11:14:52 224浏览 收藏
如果 tx.Commit() 已经成功返回 nil,再调用同一个事务的 Rollback(),通常得到 sql.ErrTxDone。它表示这个 Tx 已经结束,不能再次收尾;不是“刚才提交失败”,也不会把已经提交的数据撤销。
官方地址:https://pkg.go.dev/database/sql
提交成功后,延迟回滚为什么还会报错
设想一个写入函数在 BeginTx 成功后立即注册 defer Rollback,业务语句完成后显式 Commit。函数返回时,defer 仍然执行。若清理代码把任何非 nil 错误都当成业务失败,日志就可能同时出现“提交成功”和“回滚失败”。
这两条信息不一定矛盾。提交结束了事务,而后面的回滚只是在碰一个不能继续使用的对象。应把提交调用本身的结果与收尾清理的结果分别看待。先确认实际 Commit 返回了什么,再解释 Rollback;不要反过来用清理错误改写提交结果。
本文只讨论 database/sql 的事务对象与错误处理,不展开隔离级别、SQL 方言或连接故障后的业务恢复方案。
ErrTxDone 说明的是 Tx 生命周期
Tx 表示一次事务,应以 Commit 或 Rollback 结束。结束后继续执行事务操作,会遇到 ErrTxDone。错误本身没有携带“由哪一条业务语句结束”的完整记录;已经提交和已经回滚都可能出现它。
| 调用背景 | 后续 Rollback 的解释 | 不能推断 |
|---|---|---|
| Commit 返回 nil | 事务已结束,不能再回滚 | 提交失败 |
| Rollback 已返回 nil | 不再重复回滚同一个 Tx | 曾经提交成功 |
| 只看到了 ErrTxDone | 对象已处于结束状态 | 数据库里最终是哪种业务结果 |
因此 ErrTxDone 适合用于清理分支判断,不能单独当成提交成功的凭证。在普通 ExecContext 业务路径遇到它,也不应随手忽略:这可能意味着事务所有权或调用顺序出了问题,需要找出谁提前结束了事务。

怎样保留 defer Rollback 而不覆盖主错误
defer Rollback 的价值在于覆盖提前返回路径。某条业务语句报错、业务条件不满足或其他分支退出时,函数仍会尝试释放未提交事务。成功提交后再次清理则不会撤销提交,所以不必为了避免 ErrTxDone 删除这条兜底。
真正需要改变的是错误分类。只在事务所有者的清理位置,将 ErrTxDone 当作预期的“已结束”;其他回滚错误仍然保留。下面片段展示这个判断,其中 tx 来自成功的 BeginTx,log 为标准库日志包:
// 在事务所有者的清理代码中识别“已经结束”,不要比较错误文字。
rollbackErr := tx.Rollback()
if rollbackErr != nil && !errors.Is(rollbackErr, sql.ErrTxDone) {
// 此分支记录真实的回滚错误,不能覆盖原来的业务或提交错误。
log.Printf("事务清理失败: %v", rollbackErr)
}不要将 Rollback 的结果不加区分地写回函数的返回错误。若原来的错误是提交失败,覆盖后只剩 ErrTxDone,调用方就丢失最有用的原因;若提交成功,覆盖还会把原本成功的调用变成失败。错误链应保留主操作信息。
Commit 返回错误后能否再回滚补救
不要把“提交返回错误”理解为“同一个 Tx 还可以继续操作”。当前标准库实现在调用底层提交前会推进自身的结束状态,因此底层提交报错后,同一 Tx 后续回滚也可能只得到 ErrTxDone。它并不负责提供再次补救的通道。
也不要扩大为“任意 Commit 错误都对应同一种数据库结果”。例如关联上下文取消时,提交可能在不同位置返回错误。仅从这一次后续 Rollback 的返回值,无法还原远端发生了什么。应保留最初 Commit 错误和相关业务标识,并按具体数据库、驱动与业务契约处理。
函数重试与事务重试是另外一层决策。已经结束的 Tx 不能重新开始;再次处理需要新的事务,但是否允许重复业务动作,不能只靠 ErrTxDone 决定。

把事务收尾集中到一个函数
下面是一份可集成到项目中的事务包装函数。它接收已配置驱动的 sql.DB 和业务回调,不内置数据库连接信息,也不提供一段假想的 SQL 执行记录。回调必须把事务操作放在传入的 tx 上,并把收尾权留给 WithTx。
package transaction
import (
"context"
"database/sql"
"errors"
"fmt"
)
// WithTx 拥有事务收尾权;work 只能使用 tx,不能自行提交或回滚。
// errors.Join 需要 Go 1.20 或更新版本。
func WithTx(ctx context.Context, db *sql.DB,
work func(context.Context, *sql.Tx) error) (retErr error) {
if db == nil || work == nil {
return errors.New("数据库连接池与事务回调不能为空")
}
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return fmt.Errorf("开始事务: %w", err)
}
defer func() {
rollbackErr := tx.Rollback()
if rollbackErr == nil || errors.Is(rollbackErr, sql.ErrTxDone) {
return
}
// 保留主错误,并补充实际清理错误;不把 ErrTxDone 加进去。
cleanupErr := fmt.Errorf("回滚清理: %w", rollbackErr)
retErr = errors.Join(retErr, cleanupErr)
}()
if err := work(ctx, tx); err != nil {
return fmt.Errorf("事务业务: %w", err)
}
if err := tx.Commit(); err != nil {
// 提交错误原样保留在错误链,不由延迟回滚推断提交结果。
return fmt.Errorf("提交事务: %w", err)
}
return nil
}正常路径由 Commit 决定成功与否;提前返回路径由 defer 尝试 Rollback。如果出现非 ErrTxDone 的清理错误,用 errors.Join 补充到返回链中,不抹掉原始业务错误。代码需要 Go 1.20 或更新版本;若项目采用其他错误汇总方式,应保持同样的主错误保留原则。
这里的回调契约很关键。如果 work 私自 Commit,再返回 nil,外层再次 Commit 就会失败。把这种所有权冲突“通过忽略所有 ErrTxDone 修好”,会隐藏接口使用错误。包装函数只有一个提交点,也应只有一个明确的事务所有者。正文示例未运行,不涉及本地数据库或服务器。
几个容易混淆的收尾问题
提交成功后,defer 回滚会产生第二次数据库回滚吗?
已结束的 Tx 会由标准库状态判断拦住,返回 ErrTxDone。它不是一次撤销成功提交的操作。
能把所有 ErrTxDone 都当作成功吗?
不能。只有在明确的兜底清理位置才适合过滤;提交或业务操作遇到它,仍应检查是否重复收尾或过早使用了已结束事务。
回滚返回 nil 是否代表之前所有外部动作都撤销了?
不能。这里的事务只覆盖该数据库事务内的操作,不负责撤销回调发送的消息、写入的文件或其他外部动作。收尾设计应先明确所管理的对象范围。
-
502 收藏
-
502 收藏
-
501 收藏
-
501 收藏
-
501 收藏
-
206 收藏
-
371 收藏
-
343 收藏
-
499 收藏
-
267 收藏
-
345 收藏
-
203 收藏
-
205 收藏
-
362 收藏
-
108 收藏
-
294 收藏
-
177 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习