只设 client.timeout 会假死,因它不覆盖 dns 查询、tcp 连接、tls 握手三阶段;必须分层配置 dialcontext、tlshandshaketimeout、responseheadertimeout 及 context.withtimeout 才能防卡死。

只设 Client.Timeout 会假死,根本防不住 DNS 和 TLS 卡住
Go 的 http.Client 默认不设任何超时,Timeout 字段只从 client.Do() 开始计时,到响应体读完为止。它完全不覆盖 DNS 查询、TCP 连接建立、TLS 握手这三个前置阶段。一旦 DNS 解析卡住 30 秒,或目标服务 TLS 握手因 OCSP Stapling 失败挂起,Timeout 一丁点作用都没有。
常见现象:netstat 看到大量 SYN_SENT 或 ESTABLISHED 连接,pprof 显示数百个 goroutine stuck 在 runtime.netpoll —— 这不是并发高,是超时没配对,资源在 silently 泄漏。
-
Timeout是兜底值,建议 ≤ 30s,不能替代分层控制 - 重定向(302)默认开启,每次跳转都重新计入
Timeout,容易误超时 - 不要复用未设超时的
http.DefaultClient,它等价于Timeout: 0
DialContext 必须显式配,否则 DNS + TCP 建连永远没上限
Transport.DialContext 是防卡死的第一道闸,它控制 DNS 查询 + TCP 连接建立全过程。不用 &net.Dialer{Timeout: ...} 包一层,就等于把建连阶段完全交给操作系统——而 Linux 默认 SYN 重传最多等 3 分钟,Go 不会主动打断。
错误写法:Dial: func(...) (net.Conn, error) { c, _ := net.DialTimeout(...); c.SetDeadline(...) } —— SetDeadline 是绝对时间,复用连接时第二次请求超时会变短,且不兼容 HTTP/2。
- 必须用
(&net.Dialer{Timeout: 5 * time.Second}).DialContext,不是Dial -
KeepAlive要设(如30 * time.Second),否则空闲连接可能被 NAT 设备静默断开 - 别信“Go 1.12+ 已覆盖 TLS 握手”这种说法:DialContext 不管 TLS,它只管 TCP 层;
TLSHandshakeTimeout仍需单独设
ResponseHeaderTimeout 是黄金指标,防后端“半开”卡死最有效
很多服务端收完请求后迟迟不发响应头(比如卡在 DB 查询、锁竞争、慢日志写入),这时 Timeout 还没触发,DialContext 早已完成,goroutine 就挂在 read tcp 上不动了。ResponseHeaderTimeout 正是用来掐这个窗口:从请求发完到收到第一个字节响应头(status line + headers)的最大等待时间。
它比 Timeout 更早生效,且错误类型明确(net/http: request canceled (Client.Timeout exceeded while awaiting headers)),便于做针对性重试或降级。
- 推荐值:2–5s,比业务预期响应快一点,但留出合理网络抖动余量
- 它不包含响应体读取时间,所以大文件下载场景要额外控制
Body.Read超时 - 如果用了自定义
RoundTripper,必须检查req.Context().Done(),否则这个超时不会穿透
按接口粒度用 context.WithTimeout,别硬编码 Client.Timeout
支付回调要 30s,用户查询只要 2s,共用一个 Client.Timeout 不是省事,是埋雷。动态超时必须靠 context.WithTimeout,但它必须正确传入请求上下文,否则无效。
错误写法:client.Do(req.WithContext(ctx)) —— 这个 ctx 不会穿透到 DialContext 阶段,底层 TCP 连接仍可能无限等待。
- 必须用
http.NewRequestWithContext(ctx, method, url, body)构造请求 - 错误判断要用
errors.Is(err, context.DeadlineExceeded),不是strings.Contains(err.Error(), "timeout") - 收到
context.DeadlineExceeded后立刻返回,别再调用resp.Body.Close()或读取响应体
真正防假死的关键不在数字大小,而在是否让每个阻塞点都有独立出口:DNS、TCP、TLS、Header、Body,每一环都要有明确的 timeout 字段或 context 控制。漏掉任意一环,就等于给 goroutine 开了一扇永不关闭的门。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











