Go事务失效主因是操作未绑定到同一sql.Tx:BeginTx后必须用tx.Exec()等,禁用db.Exec();Savepoint需手写SQL;context超时不中断已执行SQL;ORM传sql.Tx仅接口兼容,未必进事务。

Go 里 BeginTx 和 Exec 不在同一个 *sql.Tx 上,事务就白做了
事务失效最常见原因:没把后续操作绑定到事务对象上。比如用 db.BeginTx() 开启事务,但后面还用 db.Exec()(属于原始 *sql.DB),那这些语句根本不在事务里,各自自动提交。
- 所有 SQL 操作必须调用
tx.Query()、tx.Exec()、tx.Prepare()等,不能混用db.Xxx() - 注意
sql.Tx不是线程安全的,别跨 goroutine 复用 - 如果用了
database/sql的连接池,tx会独占一个底层连接,直到Commit()或Rollback()
PostgreSQL 中 Savepoint 在 Go 里得手写 SQL,标准库不支持
Go 标准 database/sql 没提供 Savepoint 接口,想实现嵌套回滚或部分回滚,只能手动执行 SAVEPOINT sp1 和 ROLLBACK TO SAVEPOINT sp1。
- 必须确保
tx支持该方言(PostgreSQL/MySQL 8.0+ 可以,SQLite 行,但 MySQL 5.7 不支持SAVEPOINT) - Savepoint 名字要自己管理,避免重复;建议用
uuid.NewString()或带上下文前缀的字符串 - 执行失败时,记得检查错误是否来自 savepoint 操作本身(比如语法错),别和业务逻辑错误混淆
context.WithTimeout 传给 BeginTx,但事务超时后连接不一定立刻释放
传入 context 控制事务生命周期是对的,但要注意:超时触发后,BeginTx 会返回 error,可如果已经拿到 *sql.Tx,它的底层连接不会自动关闭,得靠连接池空闲回收或 DB 设置 SetConnMaxLifetime。
- 务必在 defer 里显式调用
tx.Rollback(),哪怕只做if tx != nil判断 - 不要依赖 context 超时来“终止正在执行的 SQL”,它只影响建连和
BeginTx阻塞,不中断已发出的tx.Exec() - 真要中断长查询,得用数据库侧机制:PostgreSQL 用
statement_timeout参数,MySQL 用max_execution_time
用 sqlc 或 ent 生成代码时,事务要自己包一层 tx,别信模板自动生成的 db 参数
很多 ORM/代码生成工具默认函数签名接收 *sql.DB,你传 *sql.Tx 也能编译过(因为 *sql.Tx 实现了 Querier 接口),但容易误以为“类型对了就自动进事务”——其实只是接口兼容,底层仍可能绕过事务控制流。
立即学习“go语言免费学习笔记(深入)”;
- 推荐做法:封装一个
Repo结构体,字段存querier interface{...},初始化时传db或tx - 检查生成的 SQL 函数是否真用了传入的
querier,有些模板会偷偷 new 一个db实例 - 单元测试里用
sqlmock时,mock 对象得按实际调用路径注册QueryRowx/Exec,否则跑不通
tx.Commit() 前某一行日志记录或 HTTP 请求就偷偷用了另一个连接。


















