context.withtimeout 必须绑定到 *http.request 而非仅传给 client.do(),否则超时不生效;正确方式是使用 http.newrequestwithcontext 或 req.withcontext;同时需配置 transport 三类底层超时且不超过 context 超时,并跨服务透传超时语义。

context.WithTimeout 传给 http.Request 而不是 http.Client
很多人写 client.Do(req) 前只调用 context.WithTimeout,却没把新 context 绑定到 *http.Request 上——这会导致超时完全不生效。Go 的 http.Client 不会主动读你传进 WithTimeout 的 ctx,它只认 request 自身的 context。
正确做法是用 http.NewRequestWithContext(ctx, ...) 或 req = req.WithContext(ctx)。漏掉这步,哪怕你设了 1 秒超时,Do() 也可能卡在 DNS 解析或 TLS 握手里十几秒。
- 别用
http.DefaultClient:它忽略所有 context,等同于裸跑 - 别对
Do()做额外包装(比如time.AfterFunc):这会绕过 HTTP 栈的取消机制,导致连接泄漏 - 错误日志里看到
net/http: request canceled但不是context.DeadlineExceeded?大概率是 request 没绑定 ctx
Transport 层必须配三类底层超时,且不能大于 context 超时
仅靠 context.WithTimeout 只能中断 goroutine,无法强制断开卡在 TCP 层的连接。如果 Transport.DialContext 或 TLSHandshakeTimeout 设得比 context 超时还长,实际耗时就会远超预期。
例如你设了 5 秒 context 超时,但 Transport.DialContext 默认是 30 秒,那请求可能先卡在 DNS 解析里 28 秒才触发 context 取消——用户早已放弃。
-
Transport.DialContext:建议 ≤ context 超时 / 3,覆盖 DNS + TCP 建连 -
Transport.TLSHandshakeTimeout:5–10s 足够,别设成 30s -
Transport.ResponseHeaderTimeout:从发完请求到收到 status line 的时间,应 ≤ context 超时 - 前两阶段预留时间 - 禁用
http.Client.Timeout(设为 0):它和 context 冲突,且语义模糊,容易掩盖真实瓶颈
跨服务调用必须通过请求头传递超时语义,下游需主动解析
context.WithTimeout 对象本身不会随 HTTP 报文发出去。上游设的 3 秒,在下游服务里根本不存在——除非你手动把超时意图编码进请求头,并由下游解析后新建自己的 context。
常见错误是上游写了 X-Request-Timeout: 3000,下游却直接用 req.Context() 启动 DB 查询,结果 DB 层用了默认 30 秒超时,整个链路就断了。
- 上游写头:
req.Header.Set("X-Request-Timeout", "3000")(单位毫秒) - 下游读头:
header.Get("X-Request-Timeout") → strconv.ParseInt → time.Duration → context.WithTimeout(parentCtx, timeout) - 解析失败时 fallback 到安全值(如
3 * time.Second),别 panic 或沿用父 deadline - gRPC 场景优先用
grpc.Timeout+grpc.WaitForReady(false),避免重试掩盖超时点
Handler 内所有阻塞操作都必须透传 req.Context(),不能新建 background
HTTP handler 里拿到的 req.Context() 已携带客户端断连、服务器全局超时等信号。一旦你用 context.Background() 或 context.TODO() 替换,这些信号就全丢了——用户关掉页面,你的 DB 查询还在跑。
典型泄漏场景:goroutine 启动后只监听 channel,却忘了 select 里同时等 ctx.Done();或者用 for range ch 读 channel,ch 关闭前 ctx 已 cancel,但循环卡死不动。
- DB 查询必须用
db.QueryContext(ctx, ...),不是db.Query(...) - 启动子 goroutine 时,参数里必须带
ctx,并在内部select中监听ctx.Done() - 别用
time.Sleep或time.After:改用select { case - 记得
defer cancel(),但更稳妥的是在每个 return 路径前显式调用cancel()
关键点不在“加超时”,而在于每个环节是否真正感知并响应了上一环的 deadline。漏掉一次 WithContext、少一个 select 分支、或错用 Background(),整条链路的超时控制就形同虚设。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











