go中无原生嵌套事务,需用savepoint手动模拟;标准库不支持,须通过exec执行savepoint/rollback to/release语句,注意命名唯一性、回滚后事务仍活跃、释放仅清理资源。

Go 里没有真正的嵌套事务,只有 savepoint 模拟
PostgreSQL、MySQL(8.0+)和 SQLite 支持 SAVEPOINT,但 Go 标准库 database/sql 不提供原生 savepoint 接口。你不能靠 tx.Begin() 嵌套出新事务——它会 panic 报 "sql: transaction already in progress"。所有“嵌套事务”行为必须手动用 SAVEPOINT SQL 语句 + Exec 实现,且需数据库驱动支持(pgx、mysql、sqlite3 均可)。
用 Exec 手动管理 SAVEPOINT 的具体写法
核心是:在已有 *sql.Tx 上执行 SAVEPOINT name、ROLLBACK TO SAVEPOINT name、RELEASE SAVEPOINT name。注意三点:
-
SAVEPOINT名字必须唯一,建议用短随机字符串或调用栈标识(如"sp_123"),避免嵌套时重名 - 回滚到 savepoint 后,该点之后的变更失效,但事务仍活跃,可继续执行其他语句
-
RELEASE SAVEPOINT只是释放保存点资源,不影响数据;不 release 也不报错,但可能占用服务端状态
示例(PostgreSQL + pgx):
tx, _ := db.Begin()
defer tx.Rollback()
<p>tx.Exec("SAVEPOINT sp<em>inner")
</em>, err := tx.Exec("INSERT INTO users(name) VALUES($1)", "alice")
if err != nil {
tx.Exec("ROLLBACK TO SAVEPOINT sp_inner") // 撤销上一步
}
tx.Exec("RELEASE SAVEPOINT sp_inner")</p><p>// 后续仍可正常提交
tx.Commit()
</p>
封装 savepoint 逻辑避免重复出错
手写 SAVEPOINT 容易漏 RELEASE 或写错名字。推荐封装成函数,带 defer 清理:
- 函数接收
*sql.Tx和 savepoint 名,返回一个 cleanup 函数(用于 rollback 或 release) - 业务代码用
defer cleanup()确保释放,无论成功失败 - 若想“静默吞掉”某段错误而不影响外层,就在 defer 前显式调用
RollbackTo()
简化版封装示意:
func WithSavepoint(tx *sql.Tx, name string) (rollback func(), release func()) {
tx.Exec(fmt.Sprintf("SAVEPOINT %s", name))
rollback = func() { tx.Exec(fmt.Sprintf("ROLLBACK TO SAVEPOINT %s", name)) }
release = func() { tx.Exec(fmt.Sprintf("RELEASE SAVEPOINT %s", name)) }
return
}
<p>// 使用
rollbackInner, releaseInner := WithSavepoint(tx, "sp_auth")
defer releaseInner() // 正常路径自动 release</p><p>_, err := tx.Exec("INSERT INTO auth_tokens ...")
if err != nil {
rollbackInner() // 出错时撤回到 savepoint
// 继续处理其他逻辑,tx 仍可用
}
</p>
不同数据库对 SAVEPOINT 的兼容性差异
不是所有数据库都一视同仁:
- PostgreSQL:完全支持,savepoint 可跨 DML/DCL,支持命名冲突检测
- MySQL:5.7+ 支持,但
ROLLBACK TO SAVEPOINT会释放该点之后创建的所有 savepoint(即不支持“子 savepoint”) - SQLite:支持,但不支持在
CREATE TABLE等 DDL 后使用 savepoint(会报"cannot rollback - no transaction is active") - SQL Server:不支持标准
SAVEPOINT语法,需用SAVE TRANSACTION name,且database/sql驱动通常不默认启用
跨数据库项目务必在初始化时检查 tx.Exec("SELECT 1") 后立即试跑 SAVEPOINT,失败则降级为全事务回滚逻辑。











