c.request.context()本身可取消,但子goroutine收不到cancel信号是因为未基于它派生子context或未监听ctx.done();常见错误包括直接用context.background()启动goroutine、用context.withvalue包装但父ctx不可取消、或子goroutine中只读不监听done通道。

直接用 c.Request.Context() 启动子 goroutine 是安全的,但一旦你在里面调用 context.WithCancel(context.Background()) 或漏监听 ctx.Done(),取消信号就断了——上游 HTTP 请求一关闭,你的后台任务还在跑,资源不释放,协程不退出。
为什么 c.Request.Context() 本身可取消,但子 goroutine 还是收不到 cancel?
因为 Gin 的 c.Request.Context() 确实是可取消的(底层绑定到 HTTP 连接生命周期),但它只对「直接监听它的 goroutine」生效。常见错误包括:
- 在 handler 里写
go doWork(context.Background())—— 彻底脱离请求上下文树 - 用
context.WithValue(c.Request.Context(), key, val)后传给子 goroutine,却没检查父 ctx 是否真的可取消(ctx.Err()为 nil 不代表它支持取消) - 子 goroutine 里只读
,但没做 <code>errors.Is(ctx.Err(), context.Canceled)判断,导致把取消当成普通错误重试
context.WithTimeout 和 context.WithCancel 派生时,parent 必须是上游传入的 ctx
你不能在函数内部“重造轮子”:context.WithCancel(context.Background()) 创建的子 ctx 和请求完全无关。正确做法是:
- 所有派生操作必须基于
c.Request.Context()或其已派生的子 ctx(比如之前用WithTimeout得到的) - 若需手动触发取消,用
ctx, cancel := context.WithCancel(c.Request.Context()),然后在合适时机调用cancel() - 若只是加超时,优先用
context.WithTimeout(c.Request.Context(), 3*time.Second),它自动继承取消链路
注意:context.WithTimeout 返回的 cancel 函数必须显式调用,否则子 ctx 不会提前终止;Gin 不会帮你自动调用。
GORM 查询报 context canceled 错误,不是 GORM 的锅
GORM v1.25+ 已支持传入 context.Context,但仅用于透传给底层 driver(如 database/sql)。真正决定是否返回 context.Canceled 的是数据库驱动和连接池行为。常见诱因:
- HTTP 请求已断开,但你还在用原始
c.Request.Context()调用db.WithContext(ctx).First(&u) - 没做错误分类:把
ctx.Err()当成网络错误重试,实际该立即清理并返回 - 在
defer里关 DB 连接,但 goroutine 因 panic 提前退出,defer不执行 —— 关键清理逻辑必须放在case 分支内
errgroup.Group 里每个 goroutine 都得自己监听 ctx.Done()
errgroup.WithContext(c.Request.Context()) 只是设置 group 的默认 ctx,它不会自动注入到 group.Go() 的函数参数里。你必须显式把 ctx 传进去,并在函数体内监听:
g, _ := errgroup.WithContext(c.Request.Context())
g.Go(func() error {
select {
case <p>最容易被忽略的一点:取消传播不是“设个 ctx 就完事”,而是每个并发分支都得主动检查、响应、清理——哪怕只差一个 goroutine 没监听,整个请求链路就存在泄漏风险。</p>











