context.WithTimeout不能跨HTTP边界传递超时,必须通过请求头显式传递并由下游解析后新建context;HTTP请求需用http.NewRequestWithContext绑定,Transport层还需配置三类底层超时以避免TCP层卡住。

context.WithTimeout 本身不能跨 HTTP 边界传递超时,你设的 5 秒在下游服务里根本不存在——必须靠请求头显式传递语义,并由下游自己创建新 context。
HTTP 请求中必须用 http.NewRequestWithContext 绑定 context
只设 context.WithTimeout 不够,不绑定到 request 对象上,超时信号压根进不了网络栈。
-
http.NewRequest创建的 request 默认无 context,Do()会忽略你声明的超时 - 必须用
http.NewRequestWithContext(ctx, ...)或req = req.WithContext(ctx) - 调用
client.Do(req)后,若超时触发,ctx.Err()会是context.DeadlineExceeded,但仅当 request 真正绑定了该 ctx - 错误处理要区分:是
ctx.Done()导致的失败,还是 DNS 失败、连接拒绝等底层错误
HTTP 客户端 Transport 必须配三类底层超时
光靠 context 超时只能中断 goroutine,TCP 层卡住(如 SYN 重传、TLS 握手慢)时,实际耗时仍可能远超设定值。
-
Transport.DialContext:控制建连(含 DNS),建议 ≤ 总超时的 1/3 -
Transport.TLSHandshakeTimeout:通常 5–10s 足够,别设成 30s -
Transport.ResponseHeaderTimeout:从发完请求到收到 status line 的时间,应 ≤ 剩余超时 - 不要同时设
http.Client.Timeout和 context 超时,前者是兜底,且语义模糊,容易掩盖问题
跨服务传递超时必须编码进请求头,下游主动解析
context 对象不会随 HTTP 报文自动传输,下游收不到你的 ctx.Deadline(),只能靠约定头解析出“还想活多久”。
- 上游写头:
req.Header.Set("X-Request-Timeout", "4500")(毫秒) - 下游读头:
header.Get("X-Request-Timeout")→strconv.ParseInt→time.Duration→context.WithTimeout(parentCtx, timeout) - 单位必须一致:Go 的
time.Millisecond是int64,别用 float 或秒级字符串直接 parse - 如果下游没读这个头,或解析失败,默认 fallback 到一个安全值(比如 3s),而不是沿用父 context 的 deadline
gRPC 场景下优先用 grpc.Timeout 和 grpc.WaitForReady(false)
gRPC Go 客户端对 context 超时支持较好,但默认 WaitForReady=true 会导致超时前反复重试连接,掩盖真实超时点。
- 显式传
grpc.WaitForReady(false),避免因连接抖动导致超时被拉长 - 用
grpc.Timeout(3 * time.Second)代替纯 context 超时,它会透传到服务端并参与 deadline 计算 - 服务端需检查
ctx.Err() == context.DeadlineExceeded并及时返回,不能忽略 - HTTP/2 流复用下,单个 stream 超时不影响其他 stream,但 transport 层仍需配好
KeepAlive和IdleTimeout
最容易被忽略的是:下游服务是否真的解析了超时头、是否用解析结果新建了 context、是否在所有 I/O 操作(DB 查询、HTTP 调用、channel receive)中都传入了这个新 context——漏掉任意一环,全链路就断了。


















