必须显式配置三类超时,仅设 http.Client.Timeout 无法覆盖 DNS 查询、TCP 连接建立和 TLS 握手等前置阶段,易致 goroutine 泄漏;它仅从 client.Do() 开始计时至响应体读完,不包含前述阻塞点。

必须显式配置三类超时,只设 Timeout 字段无法覆盖 DNS、建连、TLS 握手等前置阶段,极易导致 goroutine 泄漏。
为什么 http.Client.Timeout 单独设置不够用
它只从 client.Do() 开始计时,到响应体读完为止;不包含 DNS 查询、TCP 连接建立、TLS 握手这三个阻塞点。一旦 DNS 解析卡住 30 秒,或目标服务器防火墙丢包导致 connect() 挂起,Timeout 完全不生效。
常见现象:netstat 看到大量 SYN_SENT 或 ESTABLISHED 连接,pprof 显示数百个 stuck 在 runtime.netpoll 的 goroutine。
-
Timeout是兜底值,建议 ≤ 30s,不能替代分层控制 - 若同时设置了
Transport级超时,Timeout会覆盖它们——除非你把它设为 0 - 重定向(302)默认开启,每次跳转都重新计入
Timeout,容易误超时
必须配齐的三个 Transport 超时字段
真正防卡死,得让 http.Transport 主动中断各阶段系统调用:
立即学习“go语言免费学习笔记(深入)”;
-
DialContext:用&net.Dialer{Timeout: 5 * time.Second}控制 DNS + TCP 建连,这是最常被忽略的一环 -
TLSHandshakeTimeout:HTTPS 必须显式设,如3 * time.Second,否则默认 10s,在弱网下握手失败前会白等很久 -
ResponseHeaderTimeout:从发完请求到收到HTTP/1.1 200 OK的时间上限,防服务端“写一半就挂”,推荐 2–5s
示例配置:
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
client := &http.Client{
Timeout: 10 * time.Second,
Transport: &http.Transport{
DialContext: (&net.Dialer{
Timeout: 5 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
TLSHandshakeTimeout: 3 * time.Second,
ResponseHeaderTimeout: 3 * time.Second,
IdleConnTimeout: 30 * time.Second,
},
}单次请求要用 context.WithTimeout,且必须传给 http.Request
硬编码 Client.Timeout 不灵活,比如查用户要 2s,支付回调要 30s。此时应优先用 context 动态控制:
- 必须调用
http.NewRequestWithContext(ctx, ...),而不是先NewRequest再补WithContext - 错误判断要区分
context.DeadlineExceeded和网络错误,收到前者应立刻返回,别再读resp.Body - 如果用了自定义
RoundTripper,需确保它检查req.Context().Done()
错误写法:client.Do(req.WithContext(ctx)) —— 这样 ctx 不会穿透到 DialContext 阶段。
绝对不要碰 http.DefaultClient
http.DefaultClient.Timeout = 5 * time.Second 看似省事,但无效:它的 Transport 是私有结构,未初始化任何超时参数,且全局修改会污染所有依赖默认 client 的第三方库。
更危险的是:有人在中间件里偷偷替换 http.DefaultClient.Transport,结果导致日志上报、metrics 上报等基础组件超时行为异常,排查成本极高。
真正稳定的写法是每个业务场景声明自己的 *http.Client 实例,并明确绑定超时配置——哪怕只是复用同一个 Transport,也比共享 DefaultClient 安全。

















