http.client.timeout仅是总耗时上限,不控制dns、建连、tls等前置阶段,必须通过transport显式配置dialcontext、tlshandshaketimeout和responseheadertimeout,且body读取需额外用context控制。

http.Client.Timeout 不是默认超时,只是总耗时上限
很多人以为 http.Client.Timeout 是“默认超时”,其实它压根不生效——零值 http.Client{} 完全没有超时逻辑,可能卡死几分钟。官方文档里说的“30 秒”只是底层 net 包某些兜底重试的内部行为,和你发的每一次 Do() 无关。
这个字段真正作用是:从 client.Do() 调用开始,到响应体(Response.Body)读完为止的**总时间上限**。它覆盖 DNS、连接、TLS、发请求、收 header、读 body 全流程。
- 如果后端返回了 status line 和 headers,但 body 一直不发(比如流式接口卡住),
Timeout会等到 body 读完才触发,而不是收到 header 就停 - 它不能区分阶段;一旦设了,就无法单独放宽 DNS 或收紧 header 等环节
- 值建议设为比各阶段超时之和略大,例如
DialContextTimeout=4s + ResponseHeaderTimeout=3s→Timeout=10s
用 http.Transport 控制连接与响应头阶段超时
要防住“连得上但卡在握手”或“连上了却不发 header”这类问题,必须动 http.Transport。它的三个关键字段互不干扰,各自管一段:
-
DialContextTimeout:只管 TCP 连接建立(含 DNS 解析),推荐3–5s。设太短会误杀高延迟 DNS,太长则拖慢失败判定 -
TLSHandshakeTimeout:仅对 HTTPS 生效,控制 TLS 握手耗时,一般3–5s足够 -
ResponseHeaderTimeout:从连接完成起,到完整收到 response status line + headers 的时间上限,推荐2–4s。这是防“假连接真挂起”的核心
注意:ResponseHeaderTimeout 对 body 读取完全无效;body 超时得靠 context 或手动 io.ReadFull 控制。
单次请求优先用 context.WithTimeout,别只靠 Client.Timeout
当你需要对某一次调用精确中断(比如不想让 DNS 吃掉一半超时预算),context.WithTimeout 是更可靠的选择。它能穿透整个调用链,在 DNS 查询、connect、TLS、甚至 write request 阶段都可取消。
- 必须写
req = req.WithContext(ctx),否则新 context 不生效(常见漏赋值 bug) - 记得
defer cancel(),否则 context 泄漏,goroutine 持续堆积 - 错误判断要用
errors.Is(err, context.DeadlineExceeded),别用字符串匹配"timeout"—— 后者在不同 Go 版本或 transport 下输出不稳定
Body 读取超时必须手动加 context,Transport 不管它
http.Transport 的所有超时字段都不涉及 Response.Body 的读取过程。如果后端返回 header 后迟迟不发 body(比如 chunked 编码卡住、服务 hang 住),哪怕你设了 ResponseHeaderTimeout,程序仍会无限等下去。
解决办法只有两个:
- 用带 timeout 的
context包裹整个读取过程:io.CopyN(dst, resp.Body, n)配合ctx.Done()监听 - 或用
bufio.Reader+Read+ctx.Err()手动分块读,并在每次读前检查 context 是否已取消
这也是最容易被忽略的一环:配置了全套 transport 超时,却忘了 body 本身才是最常卡住的地方。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











