context.canceled 和 context.deadlineexceeded 不会同时出现,ctx.err() 只返回其一:显式调用 cancel() 时返回 canceled;withtimeout/withdeadline 到期时返回 deadlineexceeded。

context.Canceled 和 context.DeadlineExceeded 到底谁先出现?
它们不会同时出现,ctx.Err() 只会返回其中一种 —— 关键看取消信号的来源。如果调用方显式执行 cancel()(比如 HTTP 客户端被 Ctrl+C 中断、服务优雅关闭时调用 server.Shutdown()),就返回 context.Canceled;如果只是到了 WithTimeout 或 WithDeadline 设定的时间点,就返回 context.DeadlineExceeded。
注意:http.Client 的 Timeout 字段会覆盖传入的 context deadline,所以即使你用了 context.WithTimeout(ctx, 10*time.Second),但 client 自身设了 Timeout: 3 * time.Second,最终看到的大概率是 context.DeadlineExceeded,且错误实际由 client 内部触发,不是你的 context 主动 cancel。
为什么 handler 里刚拿到 r.Context() 就已 cancel?
Go 的 http.Server 在连接建立后就为请求创建了 context,并在以下任一时刻自动调用 cancel():
- 客户端断开连接(浏览器关页、移动端切后台、curl 被中断)
- 读取请求头或请求体超时(
ReadHeaderTimeout/ReadTimeout触发) - 响应写入完成或失败(比如下游挂了,你写 response 时 client 已掉线)
这意味着:不要假设 r.Context() 一定“新鲜”。if err := ctx.Err(); err != nil 应该是 handler 第一行逻辑,而不是等 DB 查询跑完才检查。否则你会看到日志里耗时仅 2ms 却报 context.Canceled —— 实际是 client 连上来就走了。
如何区分取消原因并做差异化处理?
Go 1.20+ 提供了 context.WithCancelCause,让取消不再是个“黑盒”:
旧写法只返回默认 context.Canceled,无法判断是超时、网络抖动还是服务主动下线;新写法可以带具体原因:
ctx, cancel := context.WithCancelCause(parent)
defer func() { cancel(nil) }() // 正常结束
<p>if err := callDownstream(ctx); err != nil {
cancel(fmt.Errorf("下游调用失败: %w", err))
return err
}
</p>
后续通过 errors.Is(err, context.Canceled) 判断是否取消,再用 context.Cause(ctx) 提取原始原因,就能决定是静默丢弃、告警、还是重试其他节点。
没升级到 Go 1.20 的项目,至少别把 context.Canceled 和业务错误混在一起返回 —— 它们语义完全不同:前者是控制流信号,后者是业务异常。
哪些操作必须传 context 并检查错误?
所有可能阻塞的 I/O 或等待行为,否则 context 信号就断在半路了:
-
http.Do()、http.Get()(注意:必须用http.NewRequestWithContext()构造 request) -
db.QueryRowContext()、tx.ExecContext()等带 Context 后缀的数据库方法 -
time.Sleep()改成select+ctx.Done()等待 - 自定义 long-running 函数中,不能只在开头
select { case ,中间计算也要定期轮询 <code>ctx.Err()
最容易被忽略的是:HTTP handler 里读 r.Body 本身就会阻塞,而 r.Body.Read() 不接受 context —— 必须用 io.CopyN 配合 ctx.Done() 做非阻塞读,或改用 http.MaxBytesReader 限流。











