Go语言无真正嵌套事务,database/sql及pgx/mysql驱动均不支持tx.Begin()嵌套,因其*sql.Tx为单层状态机,重复Begin会panic;所谓“嵌套”实为ORM(如GORM)通过SAVEPOINT模拟。

Go 语言没有真正的嵌套事务,所有“嵌套”行为都是靠数据库的 SAVEPOINT 模拟出来的;直接在已有事务上调用 tx.Begin() 必然 panic 或报错,比如 "sql: transaction already in progress"。
为什么 database/sql 和 pgx/mysql 驱动都不支持 tx.Begin() 嵌套
标准库 database/sql 的 *sql.Tx 是单层状态机:一旦调用 Begin(),内部标记为 “in transaction”,再次调用就拒绝。pgx、mysql 等驱动只是透传该限制,并非能力缺失。你看到的“嵌套事务成功”,其实是上层 ORM(如 GORM)悄悄帮你把 SAVEPOINT 语句塞进了 Exec,不是新开事务。
-
tx.Begin()在已有事务中会立即 panic,不依赖数据库类型 - PostgreSQL 报错类似
"pq: SAVEPOINT can only be used in transaction blocks",是因为驱动尝试执行SAVEPOINT但上下文不是事务块——说明它连 savepoint 都没走到那步 - MySQL 8.0+ 和 PostgreSQL 支持
SAVEPOINT,但 SQLite 不支持命名 savepoint(只支持无名SAVEPOINT+ROLLBACK TO),这点容易在多环境部署时翻车
GORM 的 SavePoint("sp_name") 到底做了什么
GORM 的 tx.SavePoint("sp1") 并非魔法,它只是封装了 tx.Exec("SAVEPOINT sp1");同理,tx.RollbackTo("sp1") 就是 tx.Exec("ROLLBACK TO SAVEPOINT sp1")。关键在于:它不自动管理命名冲突,也不保证 defer 清理。
- 名字重复会导致
"duplicate savepoint name"错误,尤其在并发调用同一函数时 -
ROLLBACK TO SAVEPOINT后事务仍活跃,后续语句继续执行,但不能对 savepoint 调用Commit() -
RELEASE SAVEPOINT只是释放服务端资源,不写数据、不改状态,漏掉也不会报错,但长期运行可能积压服务端内存(PostgreSQL 有上限) - GORM 默认启用
DisableNestedTransaction: false,但若手动配置为true,SavePoint方法会静默失效,且无提示
自己封装 RunInSavepoint 时必须绕开的坑
手写 savepoint 辅助函数比直接拼 SQL 安全,但极易在错误传播和资源清理上出问题。最常见的是 recover 没捕获到 panic、defer 执行时机不对、或闭包里误调 tx.Commit()。
立即学习“go语言免费学习笔记(深入)”;
- 不要用
defer func() { tx.Exec("ROLLBACK TO SAVEPOINT ...") }()—— 如果闭包里已经 panic,defer 里的Exec可能失败且无法感知 - 生成保存点名建议用
"sp_" + uuid.NewString()[:8],避免用固定字符串或调用栈(runtime.Caller开销大且不可靠) - 闭包函数签名必须是
func() error,且内部严禁调用tx.Commit()或tx.Rollback(),否则破坏外层事务生命周期 - 如果底层用的是
*sql.Tx(非 GORM),需自行处理 driver 差异:MySQL 要求SAVEPOINT名不含特殊字符,PostgreSQL 允许下划线但长度上限更严
真正难的不是写对 savepoint,而是判断哪一层该设点、哪一层该回滚、哪一层该让错误穿透出去——这取决于业务语义,不是技术接口能决定的。命名冲突、panic 捕获遗漏、driver 兼容性差异,这三个点在线上高频并发场景下最容易突然爆发。


















