db.withcontext(ctx)是gorm超时控制唯一有效入口,必须显式调用;defaulttransactiontimeout仅兜底未传ctx的场景,事务须从withcontext开始绑定且生命周期严格依附ctx。

db.WithContext() 是唯一有效的入口
别用 DefaultTransactionTimeout 或全局配置“碰运气”——GORM 只在你显式调用 db.WithContext(ctx) 时才真正把超时信号透传给底层驱动。一旦你写了 db.Find(&u) 这种没带 context 的调用,GORM 就会 fallback 到默认行为(通常无超时),哪怕你前面已经设了全局 timeout。
常见错误写法:db.Transaction(func(tx *gorm.DB) error { return tx.Find(&u).Error }) —— 这里 tx 没绑定任何 context,整个事务对超时完全不敏感。
- 正确姿势是:先
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second),再db.WithContext(ctx).Transaction(...) - 所有单条语句也一样:
db.WithContext(ctx).First(&u, 1),不是db.First(...)后再塞 context -
WithContext()返回新实例,原db不变,不可复用;每次调用都要重新 wrap
事务必须从 BeginTx 开始绑定 context
如果你用原生 database/sql 驱动(比如直接操作 *sql.DB),Begin() 不接受 context,但 BeginTx(ctx, opts) 才是事务与超时联动的唯一合法起点。GORM 底层也是走这条路,所以你看到的 db.WithContext(ctx).Transaction() 实际上就是封装了 BeginTx 调用。
- 事务生命周期必须和 context 生命周期严格一致:context 超时后,
tx.Commit()或tx.Rollback()仍可能成功,但数据状态已不可信 - 超时后必须立刻检查
ctx.Err()并手动tx.Rollback(),不能等 defer 或靠 GORM 自动收尾 - 子查询、Savepoint 等嵌套操作,必须复用同一个
ctx,不能在事务函数里新建context.Background()
HTTP 请求中传入的 context 容易被协程“偷走”
Gin 或其他 Web 框架传进来的 c.Request.Context() 是请求级上下文,生命周期随 HTTP 连接结束而 cancel。一旦你在 handler 里起野生 goroutine(比如 go func() { db.WithContext(c.Request.Context()).Save(...) }()),这个协程大概率会在响应返回后收到 context canceled 错误。
- 异步任务必须脱离请求 context:用
context.Background()+ 自定义 timeout,或接入任务队列(如 Asynq) - 如果非要复用请求 context,请确保协程在 request 结束前完成,或用
context.WithCancel(parent)显式延长生命周期 - 日志里频繁出现
context canceled且伴随sql: transaction has already been committed or rolled back,基本可锁定是野生协程问题
慢查询监控依赖 Trace 中的 context 计时
仅靠 WithContext() 设置超时还不够,你得知道哪条 SQL 真的慢了。GORM 的 Trace() 方法接收的 ctx 是带 DB 连接信息的,可以安全地用 context.WithValue(ctx, keyStart, time.Now()) 埋点。
- 别在
Info()或Warn()里用time.Now()记耗时——这时 SQL 已执行完,拿到的是“结果时间”,不是“执行时间” -
SlowThreshold需显式配置,且日志级别至少为Warn,否则不会触发Trace().End() - fc() 是延迟求值闭包,只在判定为慢查时才解析 SQL 字符串,避免无谓开销











