db.Transaction() 是最安全的跨函数事务入口,应优先用于90%业务场景;手动Begin()需显式传递*gorm.DB参数、绑定超时ctx、禁止存入context,确保事务原子性。

db.Transaction() 是最安全的跨函数事务入口
90% 的业务场景(如下单、扣库存、写日志)不该手动 Begin(),直接用 db.Transaction() 就行。它自动处理 panic 捕获、error 回滚、成功提交,不用写 defer tx.Rollback(),也不怕忘记 Commit() 导致连接卡死。
常见错误是把 db.Transaction() 当成“开启事务”的动作,然后在回调里再传 tx 给其他函数——其实根本不需要:所有需要事务的操作,都该在回调函数体内完成,tx 本身只在闭包内有效。
- 正确姿势:
db.Transaction(func(tx *gorm.DB) error { return service.DoSomething(tx) }) - 错误姿势:先
tx := db.Begin(),再service.DoSomething(tx),最后自己管Commit()/Rollback() - 如果必须跨函数(如 service → repo 分层),才考虑
BeginTx()或Begin(),但必须确保每个函数都显式接收*gorm.DB参数,且调用链不中断
手动 Begin() 后如何保证事务对象不丢失
手动调用 db.Begin() 或 db.BeginTx(ctx, opts) 后,*gorm.DB 实例(即事务对象)必须作为参数一级级传下去,不能靠全局变量、闭包捕获或 context 存储。
容易踩的坑是:在 handler 里 tx := db.Begin(),然后调用 service.CreateOrder(tx),但 service 内部又调用了另一个 repo 函数却没传 tx,导致部分操作落在原始 db 上,事务原子性彻底失效。
- 所有参与事务的函数签名必须含
tx *gorm.DB参数,例如:func (s *Service) CreateOrder(tx *gorm.DB, order Order) error - repo 层不能依赖
db全局实例,而应接收tx并调用tx.Create()等方法 -
tx不可缓存进 struct,否则 goroutine 泄漏 + 取消信号失效
Context 超时必须从 BeginTx 开始绑定
用 db.Begin() 无法传递超时信号;必须用 db.BeginTx(ctx, nil),且该 ctx 必须已带合理超时(如 context.WithTimeout(r.Context(), 30*time.Second))。
如果事务外建一个超时 ctx,再传给单个 QueryContext(),事务本身仍会卡住——哪怕上下文已过期,tx.Commit() 还能执行,锁不释放、数据不一致。
- 错误:
tx := db.Begin(); dbCtx, _ := context.WithTimeout(ctx, 2*time.Second); tx.WithContext(dbCtx).First(&u) - 正确:
tx, err := db.BeginTx(ctx, &sql.TxOptions{}); if err != nil { ... } - GORM 的
DefaultTransactionTimeout不生效于显式传入的ctx,所以别指望全局配置兜底
context.WithValue 不能用来传事务对象
别把 *gorm.DB 塞进 context,也别用 context.WithValue 传 tx。Context 是为元数据(traceID、userID)设计的,不是函数参数中转站。
强行塞会导致:类型失控、测试困难、调用链断裂、静态分析失效。而且 *gorm.DB 不可比较,作为 key 会 panic;作为 value 会被误当成普通结构体拷贝,实际是引用,极易引发并发问题。
- 绝对禁止:
ctx = context.WithValue(ctx, txKey, tx) - 取值时若用
ctx.Value(txKey).(*gorm.DB)强制断言,一旦 key 冲突或未传,运行时 panic - 真正该进 context 的只有不可变轻量元数据,比如
type traceIDKey struct{}+ctx.Value(traceIDKey{})
tx,就等于放弃了对执行路径的控制。


















