context.withtimeout 不强制终止 goroutine,仅通过 done() 通道发取消信号;需手动检查 ctx.err() 或 select 等待 ctx.done() 来主动退出。

context.WithTimeout 为什么没让 goroutine 真正停止?
它只负责发信号,不强制终止——context.Context 的 Done() 通道关闭后,你得自己检查并退出逻辑。常见错误是启动 goroutine 后完全不管它的执行状态,以为设了超时就万事大吉。
典型场景:HTTP 请求、数据库查询、第三方 API 调用,这些阻塞操作必须主动响应 ctx.Done()。
- 所有可能长时间运行的代码路径里,都要定期检查
ctx.Err() != nil或 select 等待ctx.Done() - 不要在 goroutine 里直接用
time.Sleep模拟耗时,而要用select { case - 调用外部库(如
http.Client)时,务必传入带 timeout 的ctx,否则底层不会感知取消
http.Client.Do 怎么配合 context 实现请求级超时?
很多人以为给 context.WithTimeout 就够了,但漏掉关键一步:把 context 传进 http.Request,而不是只传给 Do 调用本身。
正确做法是用 req.WithContext(ctx) 构造新请求;否则 http.Client 会忽略上下文,超时信号根本到不了 TCP 层或 TLS 握手阶段。
立即学习“go语言免费学习笔记(深入)”;
-
http.NewRequest返回的*http.Request默认没有绑定 context,必须显式调用WithContext -
http.Client.Timeout是整个请求生命周期上限,和 context 超时是两套机制,建议只用 context 控制,Client.Timeout 设为 0 - 如果用了自定义
http.Transport,要确保IdleConnTimeout和ResponseHeaderTimeout不干扰 context 行为
req, _ := http.NewRequest("GET", "https://api.example.com", nil)
req = req.WithContext(ctx) // 关键!
resp, err := client.Do(req)
goroutine 泄漏的隐蔽来源:忘记 close channel 或 defer cancel
每次调用 context.WithTimeout 都会返回一个 cancel 函数,不调用它,底层 timer 和 goroutine 就不会释放——这是生产环境内存缓慢增长的常见原因。
尤其在循环或高频 handler 中,漏掉 defer cancel() 几乎必然导致泄漏。
- 只要用了
WithCancel/WithTimeout/WithDeadline,就必须配对调用cancel() - 在函数退出前调用,优先用
defer cancel(),避免分支遗漏 - 不要把
cancel函数传给其他 goroutine 并在那里调用——它不是线程安全的,且可能提前触发 - 测试时可临时加
runtime.GC()+pprof.Lookup("goroutine").WriteTo观察是否持续增长
子 context 取消后,父 context 还能用吗?
能。父子 context 是单向通知关系:子 cancel 不影响父,但父 cancel 会级联取消所有子。这个特性常被误用,比如在中间件里无脑创建子 context 却没管理好生命周期。
典型踩坑:handler 中用 ctx, cancel := context.WithTimeout(r.Context(), time.Second),但没在 return 前 cancel(),导致每次请求都多一个 timer goroutine。
- 子 context 的
cancel只控制该子及其后代,不影响父 context 的有效性 - 父 context 被 cancel 后,所有子 context 的
Done()会同时关闭,无需额外处理 - 跨 goroutine 传递 context 时,别传
cancel,只传ctx;取消权必须保留在创建者手中
真正难的不是写对那几行 context 代码,而是每个 IO 操作、每层封装、每次 goroutine 启动,都得问一句:它会不会忽略 ctx.Done()?有没有人负责调 cancel()?










