Go 的 http.Client 默认无超时,所谓“30秒”是底层兜底逻辑而非请求级超时;必须显式配置 Timeout、Transport 各阶段超时或使用 context.WithTimeout 精确控制。

Go 的 http.Client 默认没有超时,别信文档里“默认 30 秒”的说法
Go 标准库的 http.Client 实例本身**不设任何默认超时**——这是很多人踩坑的起点。官方文档里提到的“30 秒”其实是底层 net/http 在某些内部场景(比如 DNS 解析失败后重试)用的兜底逻辑,**不是请求级超时**。你用零值 http.Client{} 发请求,它可能卡住几分钟甚至更久。
- 真正生效的超时必须显式配置
http.Client.Timeout,或更精细地控制Transport的各阶段超时 -
Timeout是“从Do()开始到响应体读完为止”的总耗时,包含 DNS、连接、TLS 握手、发送请求、等待响应头、读响应体全部环节 - 如果只设
Timeout,但后端返回了 header 却迟迟不发 body(比如流式接口卡住),这个超时依然会等下去——此时需要单独控Response.Body的读取行为
用 context.WithTimeout 控制单次请求,比 Client.Timeout 更灵活
当你要对某一次 http.Do() 施加超时,且希望中断时机更可控(比如不想让 DNS 查询吃掉一半时间),优先用 context。它能穿透整个调用链,在任意阶段(包括 DNS 解析、连接建立、甚至 TLS 握手)触发取消。
- 构造带超时的 context:
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - 传给 request:
req = req.WithContext(ctx),再client.Do(req) - 注意:
cancel()必须调用,否则 context 泄漏;建议用defer cancel() - 错误判断要检查
errors.Is(err, context.DeadlineExceeded),而不是字符串匹配"timeout"
http.Transport 的三个关键超时字段不能混用
当你需要区分不同阶段的超时(比如允许慢 DNS 但不允许慢响应),就得动 http.Transport。它有三个独立字段,作用完全不同:
-
DialContextTimeout:仅控制 TCP 连接建立(含 DNS 解析)的最大耗时 -
TLSHandshakeTimeout:仅控制 TLS 握手阶段最大耗时(HTTP/1.1 over TLS 或 HTTP/2) -
ResponseHeaderTimeout:从连接建立完成起,到收到完整 response header 为止的耗时上限 - 这三个字段和
Client.Timeout是正交关系;如果同时设置,以先触发者为准 - 特别注意:
ResponseHeaderTimeout不管 body 读多久——body 读取超时得靠io.CopyN或bufio.Reader.Read配合 context 自己控制
超时后连接没被复用,是正常现象,不是 bug
Go 的 http.Transport 在超时发生时,会主动关闭底层连接,并标记为“不可复用”。这不是资源泄漏,而是设计使然——超时往往意味着远端状态异常(如半开连接、中间设备丢包),复用这种连接可能导致后续请求静默失败。
立即学习“go语言免费学习笔记(深入)”;
- 如果你观察到
http.Transport.IdleConnTimeout没生效,或者连接池空闲数持续为 0,大概率是因为频繁超时把连接都关掉了 - 不要为了“复用”而盲目延长超时;先确认是网络问题、服务端卡顿,还是客户端逻辑缺陷(比如没及时读完 body 导致连接卡在 read 状态)
- 调试时可开启
GODEBUG=http2debug=2查看连接生命周期,但生产环境慎用
超时配置不是填个数字就完事——每个字段生效的位置、中断的粒度、对连接复用的影响,都得按实际链路去对。最常被忽略的是:header 收到了,body 却没读完,这时所有 transport 级超时都已失效,只剩 context 或手动读取控制能救场。


















