context.WithTimeout 是唯一能真正中断外部 API 调用的机制,因其可穿透 DNS 解析、TCP 建连、TLS 握手等底层阻塞阶段;HTTP Client 必须显式传入 context,且需通过 http.NewRequestWithContext 或 req.WithContext 传递,否则 client.Timeout 仅在 req.WithContext 调用时生效。

context.WithTimeout 是唯一能真正中断外部 API 调用的机制,其他方式(如 time.After、http.Client.Timeout 单独设置)无法终止 DNS 解析、TCP 连接建立或 TLS 握手等底层阻塞阶段。
HTTP Client 必须显式传入 context,且不能只靠 Client.Timeout
很多人设了 client.Timeout = 5 * time.Second 就以为万事大吉,结果日志里全是 net/http: request canceled 或者客户端等了 20 秒才返回——说明超时根本没起作用。
根本原因是:Client.Timeout 只在标准库内部调用 req.WithContext 时才生效;如果你手动构造 *http.Request 后直接调用 client.Do(req),而没写成 client.Do(req.WithContext(ctx)),那这个 ctx 就完全没传递下去。
- 必须用
http.NewRequestWithContext(ctx, ...)构造请求,或手动调用req.WithContext(ctx) - 自定义
http.Client时,Timeout字段只是兜底值,优先级低于传入的ctx;但若没传ctx,它才生效 - 如果用了中间件或封装 SDK(比如云厂商 Go SDK),要确认它是否透传了
ctx;很多 SDK 内部会忽略你传的ctx,得看源码或文档
Transport 层必须配 DialContext 和 ResponseHeaderTimeout
即使 req.WithContext(ctx) 写对了,如果 http.Transport 没配好,DNS 卡住、SYN 重试、TLS 握手 hang 住,照样超时不生效。
立即学习“go语言免费学习笔记(深入)”;
典型表现:请求卡在 context.DeadlineExceeded 前就已停滞,但 ctx.Done() 没触发,因为底层还没走到可取消的 I/O 阶段。
-
DialContext必须用&net.Dialer{Timeout: ...}初始化,否则连接建立阶段绕过ctx -
ResponseHeaderTimeout控制从发完请求到收到响应头的时间,防 header 卡死 -
TLSHandshakeTimeout和ExpectContinueTimeout也建议显式设,尤其调用 HTTPS 外部服务时 - 别复用未配置 timeout 的全局
http.DefaultClient,高并发下一个慢请求拖垮整条链路
熔断器(如 gobreaker)不会自动识别 context.DeadlineExceeded
gobreaker 默认只把非 nil error 当作失败,但很多 HTTP SDK 在超时后返回 nil, nil 或自定义结构体错误(比如 err.Code == "TIMEOUT"),gobreaker 就完全感知不到失败,熔断永远不触发。
这不是库的问题,是你没包装错误逻辑。
- 必须手动检查
errors.Is(err, context.DeadlineExceeded)并转为普通error - 对 SDK 返回的空响应、特定错误码、字符串匹配(如
"timeout")做显式 error 包装 - 如果 HTTP 状态码非 2xx,也要主动返回 error,否则
gobreaker不统计 - 每个下游依赖(支付、用户、库存)要用独立子
context,避免一个慢接口拖垮所有调用
别用 http.TimeoutHandler 替代业务层超时
http.TimeoutHandler 只是给 http.ServeHTTP 包了一层计时器,它拦不住 handler 里启的 goroutine、DB 查询、或外部 HTTP 调用。一旦 handler 函数体返回(哪怕只是 return),它的超时就结束了,后台任务还在跑。
常见后果:日志显示 “503 Service Temporarily Unavailable”,但下游服务仍在扣库存、发消息、写日志——这就是典型的超时失控。
-
http.TimeoutHandler适合防 handler 死循环或 panic 后卡住,不是业务超时方案 - 真正的超时控制必须下沉到每一步耗时操作:DB 用
QueryContext,Redis 用WithContext方法,自己写的异步逻辑用select { case - 如果 handler 里有
go func() { ... }(),必须把ctx传进去,并在内部监听ctx.Done()主动退出
最易被忽略的一点:cancel 函数必须 defer 调用,否则 timer 不释放,goroutine 和内存泄漏会在流量高峰时突然爆发;而这个问题在线下压测几乎不会暴露。


















