Go 的 http.Get 默认无超时,必须手动配置三阶段超时:DialContext(DNS+TCP)、TLSHandshakeTimeout(HTTPS握手)、ResponseHeaderTimeout(响应头),并配合 context.WithTimeout 兜底,且务必关闭 resp.Body。

Go 的 http.Get 默认无超时,高并发下极易导致 goroutine 卡死、连接堆积、程序静默挂起——这不是 bug,是设计使然,必须手动补全超时控制。
为什么 http.Get 会卡在 ReadHeader 阶段不返回
因为 http.Get 底层调用的是 http.DefaultClient.Get,而该客户端的 Timeout 字段为零值(0),意味着整个请求生命周期不设限。但更隐蔽的问题在于:Client.Timeout 只覆盖“连接已建立之后”的阶段(发请求、等响应头、读 body),对 DNS 查询、TCP 建连、TLS 握手完全无效。
常见卡死现象包括:
- pprof 中大量 goroutine 停在
net/http.(*conn).readLoop或net/http.readResponse -
netstat显示一堆SYN_SENT或长时间ESTABLISHED但无数据收发的连接 - 压测中
runtime.NumGoroutine()持续上涨,内存与文件描述符缓慢耗尽
http.Client.Timeout 不够用,必须配 http.Transport 的三阶段超时
只设 Client.Timeout 是典型半吊子配置,漏掉最常出问题的前置环节。真正要控住链路,这三个 Transport 字段必须显式设置:
-
DialContext:用&net.Dialer{Timeout: 5 * time.Second}控制 DNS + TCP 建连总耗时(第一道防线) -
TLSHandshakeTimeout:HTTPS 必设,推荐3 * time.Second~5 * time.Second,防 OCSP Stapling 或弱网握手卡住 -
ResponseHeaderTimeout:从连接建立完成起,到收到完整 status line + headers 的最大等待时间,推荐2 * time.Second;它不管 body 读多久,仅保 header 不拖沓
注意:IdleConnTimeout 和 MaxIdleConnsPerHost 要按实际 QPS 和下游承载力评估,盲目设大反而加剧资源争抢。
context.WithTimeout 是唯一能穿透全链路的取消机制
Client.Timeout 和 Transport 各项超时都依赖底层系统调用支持,在某些极端场景(如对方突然断连、内核 socket 缓冲区异常)仍可能失效。此时必须叠加 context.WithTimeout 作为兜底:
- 必须用
http.NewRequestWithContext(ctx, "GET", url, nil)构造请求,而不是req.WithContext(ctx)后再传给client.Do()——后者不中断 dial 阶段 - 超时时间建议略小于
Client.Timeout(例如 Client 设 10s,context 设 8s),留出误差余量 - 所有路径下都必须确保
resp.Body.Close()被调用(error 分支不能漏),否则连接无法复用,MaxIdleConns形同虚设
别忽略 ExpectContinueTimeout 和 keep-alive 探测间隔
这个字段常被遗漏,但它防的是服务端在收到 Expect: 100-continue 后迟迟不响应 continue 的情况,推荐设为 1 * time.Second。另外,KeepAlive 是 TCP 层保活探测间隔(非连接空闲超时),设太小会增加探测开销,设太大则无法及时发现对端僵死连接;生产环境建议 30 * time.Second 起步。
真正容易被忽略的点是:超时不是设了就完事——每个 http.Request 的 ctx、每个 resp.Body 的关闭、每个 Transport 字段的协同边界,都得对齐业务 SLA。比如爬虫对第三方 CSS 的容忍度可能是 2s,但对主站 HTML 可能是 5s,这种差异必须靠不同 client 实例或动态 context 来区分,不能共用一套超时参数。

















