Go的http.Client必须显式设置Timeout、DialContext和TLSHandshakeTimeout三类超时,仅用context.WithTimeout或time.AfterFunc无法终止DNS查询、TCP连接或TLS握手等底层系统调用,易致goroutine泄漏和连接堆积。

Go 的 http.Client 没有“超时函数”,只有超时字段;直接调用 http.Get 或复用 http.DefaultClient 就等于放弃超时控制——线上服务卡住几分钟甚至更久,不是偶然,是必然。
Client.Timeout 字段只管总耗时,不拦 DNS 和 TLS
Client.Timeout 是从 client.Do() 开始计时,到 resp.Body 关闭为止的总上限。它覆盖发送请求、等待响应头、读响应体,但对以下环节完全无效:
- DNS 查询阻塞(比如
/etc/resolv.conf配了不可达 DNS 服务器) - TCP 连接被防火墙拦截,
connect()卡在 SYN 重传 - TLS 握手因 OCSP Stapling 或证书链验证卡住
这些阶段一旦出问题,Client.Timeout 根本不触发,goroutine 和文件描述符持续泄漏。常见现象:pprof 里堆着几十个 stuck 在 runtime.netpoll 的 goroutine,netstat 看到大量 SYN_SENT 或 ESTABLISHED 状态连接。
Transport.DialContext 必须显式构造,不能只设 Timeout
Transport.DialContext 是唯一能中断 DNS + TCP 建连的入口。它必须用 &net.Dialer{Timeout: 5 * time.Second} 构造,而不是随便写个函数闭包。常见错误写法:
立即学习“go语言免费学习笔记(深入)”;
Transport: &http.Transport{
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
return net.Dial(network, addr) // ❌ 没设超时,等同于无限制
},
}正确写法是:
Transport: &http.Transport{
DialContext: (&net.Dialer{
Timeout: 5 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
}注意:KeepAlive 和超时无关,别误当超时参数;DialContext 超时值建议设 3–5 秒,太短会误杀高延迟 DNS,太长拖慢失败判定。
ResponseHeaderTimeout 是防“假连接真挂起”的关键
后端服务可能已接受 TCP 连接、完成 TLS 握手,但迟迟不发 HTTP status line(比如进程卡在日志刷盘或 DB 锁)。这时 Client.Timeout 还没开始计时(因为还没发请求),DialContext 已成功,TLSHandshakeTimeout 也过了——唯独 ResponseHeaderTimeout 能拦住。
它从请求写完那一刻开始计时,到完整收到 HTTP/1.1 200 OK 及所有 header 行为止。典型值设 2–4 秒。注意:ResponseHeaderTimeout 对响应体(body)读取完全无效,流式接口的 body 超时得靠 context 或 io.CopyN 手动控。
单次请求必须用 context.WithTimeout,且要传给 Request
动态控制某次调用(比如支付回调需 30 秒,查询用户只需 2 秒),优先用 context.WithTimeout。但它必须通过 http.NewRequestWithContext() 注入,而不是先 http.NewRequest() 再 .WithContext():
ctx, cancel := context.WithTimeout(context.Background(), 8*time.Second) defer cancel() // 必须 defer,否则 context 泄漏 <p>req, err := http.NewRequestWithContext(ctx, "GET", url, nil) // ✅ 正确 // req = req.WithContext(ctx) // ❌ 错误:只影响 resp.Body.Read,不中断 dial/TLS
错误判断必须用 errors.Is(err, context.DeadlineExceeded),不要字符串匹配 "timeout" —— 不同 Go 版本和 transport 下错误信息不稳定,且 context.Canceled 和 DeadlineExceeded 语义不同。
真正难的不是配几个字段,而是理解每个超时字段对应哪一段系统调用:DNS 解析走 getaddrinfo,TCP 连接走 connect(),TLS 握手走 SSL_connect(),而 Go 的 http.Transport 必须在每一层都主动设限,才能避免底层系统调用无限阻塞。


















