context.WithTimeout 不是强制终止协程,而是通过 ctx.Done() 通道通知任务自行退出;需手动监听、透传并调用 cancel(),否则超时无效且泄漏内存。

context.WithTimeout 不是“定时杀协程”,而是发信号让任务自己停
它不会中断正在运行的 goroutine,也不会强制终止系统调用(比如 read 或 write),而是通过一个只读通道 ctx.Done() 通知:“时间到了,该收尾了”。是否响应、何时退出,完全取决于你写的代码有没有监听这个通道。
-
标准库中已集成 context 的函数(如
http.Client.Do、db.QueryRowContext)会自动检查并提前返回错误 - 自定义耗时逻辑(比如轮询、计算、
time.Sleep)必须手动select监听ctx.Done(),否则超时信号永远不生效 - 如果只在函数开头检查一次
ctx.Err(),但后续有长时间阻塞操作(如未带 ctx 的db.Query),整个链路仍会卡住
超时计时从 WithTimeout 调用那一刻开始,不是从任务启动算起
很多人误以为 context.WithTimeout(ctx, 2*time.Second) 是给“接下来要做的某件事”设限,其实它是从这一行执行完就开始倒计时——哪怕你 500ms 后才真正 go doSomething(ctx),那也只剩 1.5 秒了。
- 所以,不要在启动 goroutine 前“提前很久”创建 timeout context,容易白等耗时
- 也不要在循环里反复调用
WithTimeout(比如每次重试都新建),这会泄漏 timer 和 goroutine - 正确做法:在任务入口统一创建一次,再透传给所有子调用(HTTP、DB、RPC 等)
cancel() 必须调用,否则底层 timer 不释放
context.WithTimeout 内部依赖一个运行中的 time.Timer,它不会因为超时触发就自动销毁。如果不显式调用 cancel(),即使上下文已过期,timer 仍驻留内存,长期积累会导致内存泄漏。
- 务必用
defer cancel(),哪怕任务提前成功返回 - 同一个
cancel()函数可安全调用多次,第二次起静默忽略,不怕重复 defer - 切勿把
cancel()传进 goroutine 内部并由它调用——外部无法保证它一定执行;应由创建 context 的作用域负责清理
HTTP 客户端超时 ≠ 全链路超时,必须分层控制
设置 http.Client.Timeout = 5 * time.Second 只管单次请求的连接+读写总耗时,但它无法约束“发起请求前的序列化”、“收到响应后的 JSON 解析”或“下游服务调用”的时间。真正的端到端超时,得靠外层 context.WithTimeout 统一兜底。
立即学习“go语言免费学习笔记(深入)”;
- 推荐模式:外层 context 控制整体生命周期(比如 3s),再传给
http.NewRequestWithContext,让 HTTP 底层也受其约束 - 若同时设了
Client.Timeout和外层 context,以更短的那个为准 - 注意:
http.DefaultClient没设默认超时,不加 context 就可能无限 hang 住
最常被忽略的一点:超时控制不是加一行 WithTimeout 就完事,它是一条链——从入口创建、逐层透传、每个阻塞点监听、最后统一 cancel。漏掉任意一环,超时就形同虚设。










