直接用sql.tx手动写事务容易出错,因其无自动回滚机制,panic或error时必须显式调用tx.rollback(),漏调会导致连接挂起;重试需手动判断错误类型、新建tx,逻辑冗余易错。

为什么直接用 sql.Tx 手动写事务容易出错
因为 Go 的 sql.Tx 本身不带自动回滚逻辑,一旦中间某步 panic 或返回 error,你得自己显式调用 tx.Rollback();漏掉这句,连接就卡在 pending 状态,后续请求可能被阻塞或超时。更麻烦的是,如果想重试(比如遇到死锁 ERROR 1213: Deadlock found when trying to get lock),还得手动判断错误类型、释放资源、重新建 tx —— 这些逻辑堆在一起,很快变成样板代码。
ExecuteTx 函数该怎么设计才安全可靠
核心是把「开启事务 → 执行业务逻辑 → 成功提交 / 失败回滚」封装成原子操作,并支持按错误类型决定是否重试。关键点有三个:
- 传入的函数签名必须是
func(*sql.Tx) error,让调用方专注写 SQL 逻辑,不碰事务生命周期 - 重试需限定次数(比如最多 3 次),且只对可重试错误(如死锁、事务超时)生效,不能重试约束冲突(
ERROR 1062: Duplicate entry) - 每次重试前必须关闭旧
*sql.Tx,否则连接泄漏;Go 的sql.Tx不支持复用,失败后必须新建
示例实现片段:
func ExecuteTx(db *sql.DB, maxRetries int, fn func(*sql.Tx) error) error {
for i := 0; i <h3>怎么判断一个 error 是否值得重试</h3><p>MySQL 和 PostgreSQL 的死锁错误码不同,不能硬编码字符串匹配。推荐用 <code>errors.As</code> 提取底层 driver 错误再比对:</p><div class="aritcle_card flexRow artxards">
<div class="artcardd flexRow">
<a class="aritcle_card_img" rel="nofollow" href="/xiazai/learn/7564" title="使用Go语言搭建家庭相册系统-相关课件"><img
src="https://img.php.cn/upload/webcode/000/000/164/636a2b4d84031727.png" alt="使用Go语言搭建家庭相册系统-相关课件" onerror="this.onerror='';this.src='/static/lhimages/moren/morentu.png'" ></a>
<div class="aritcle_card_info flexColumn">
<a rel="nofollow" href="/xiazai/learn/7564" title="使用Go语言搭建家庭相册系统-相关课件" class="overflowclass">使用Go语言搭建家庭相册系统-相关课件</a>
<p class="overflowclass">使用Go语言搭建家庭相册系统-相关课件</p>
</div>
<a rel="nofollow" href="/xiazai/learn/7564" title="使用Go语言搭建家庭相册系统-相关课件" class="aritcle_card_btn flexRow flexcenter"><b></b><span>下载</span>
</a>
</div>
</div>
- MySQL:检查是否为
*mysql.MySQLError,再看Number字段是否等于1213(死锁)或1205(锁等待超时) - PostgreSQL:检查是否为
*pq.Error,再看Code是否等于"40001"(serialization_failure)或"40P01"(deadlock_detected) - 其他错误(如唯一键冲突、空值约束)一律不重试,否则会无限循环
注意:不要用 strings.Contains(err.Error(), "deadlock"),生产环境日志可能被本地化或改写,不可靠。
为什么不能把重试逻辑放在业务函数内部
因为事务上下文(*sql.Tx)和数据库连接绑定,一旦 tx.Rollback() 被调用,该 tx 对象就失效了。如果在 fn 内部尝试重试,它拿到的还是已关闭的 tx,再执行 Query 会直接 panic:sql: transaction has already been committed or rolled back。
真正需要重试时,必须由外层函数新建 sql.Tx 并重新调用 fn —— 这也是为什么 ExecuteTx 必须接收一个函数值,而不是让业务代码自己管理 Begin/Commit。
实际使用中,最容易被忽略的是重试间隔和最大次数没设上限,或者把非事务性错误(比如网络超时)也纳入重试,反而放大故障影响。










