context.Context 是最可靠方式,可主动取消请求、释放连接、避免 goroutine 泄漏;http.Client.Timeout 仅控制总耗时且无法中断 DNS/TLS/连接建立,也不传播取消信号。

Go 语言中限制 HTTP 请求超时,context.Context 是最可靠、最符合 Go 生态的方式——它不只是“加个 timeout”,而是能主动取消请求、释放连接、避免 goroutine 泄漏。
为什么不能只用 http.Client.Timeout?
http.Client.Timeout 只控制整个请求的总耗时(从 RoundTrip 开始到响应 Body 可读为止),但它无法中断正在进行的 DNS 解析、TLS 握手或 TCP 连接建立;更关键的是,它不传播取消信号,下游依赖(比如自定义 RoundTripper 或中间件)无法响应中断。
而 context.Context 提供了可组合的取消机制,HTTP 客户端原生支持:http.NewRequestWithContext() 会把 context 透传到底层连接和 transport 层。
-
http.Client.Timeout是“尽力而为”的兜底,适合简单场景 -
context.WithTimeout()是“主动控制”的标准方式,适合生产环境 - 两者可以共存,但 context 的取消优先级更高
http.NewRequestWithContext() 必须显式调用
直接改 http.DefaultClient 的 timeout 不影响已发出的请求;想让 context 起作用,必须用 http.NewRequestWithContext() 创建请求对象,再传给 client.Do()。
立即学习“go语言免费学习笔记(深入)”;
常见错误是:写了 context,却仍用 http.NewRequest(),导致超时完全不生效。
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// ✅ 正确:context 会传入 transport 并参与 DNS/TLS/连接阶段
req, err := http.NewRequestWithContext(ctx, "GET", "https://api.example.com", nil)
// ❌ 错误:context 被丢弃,超时只靠 client.Timeout 控制
// req, err := http.NewRequest("GET", "https://api.example.com", nil)
client := &http.Client{}
resp, err := client.Do(req)
超时时间要覆盖哪些阶段?
一个典型 HTTP 请求包含多个阶段:DNS 查询 → TCP 连接 → TLS 握手 → 发送请求 → 等待响应头 → 读取响应体。不同 context 超时设置影响不同环节:
-
context.WithTimeout():从Do()调用开始计时,覆盖全部阶段(包括阻塞在Read()上的响应体读取) -
http.Client.Timeout:同样覆盖全程,但无取消传播能力 -
http.Client.Transport.DialContext:可单独控制 DNS + TCP 建连耗时(需自定义net.Dialer) -
http.Client.Transport.TLSHandshakeTimeout:仅控制 TLS 握手,对 HTTP/1.1 有效,HTTP/2 下可能被忽略
推荐做法:用 context.WithTimeout() 统一控制整体,再根据需要微调 transport 层参数(如防止 DNS 拖慢整个请求)。
容易被忽略的陷阱:response.Body 没 close 导致 context 不释放
即使 context 超时触发了取消,如果代码没调用 resp.Body.Close(),底层连接可能不会立即归还到连接池,甚至引发 goroutine 泄漏(尤其在重试或长连接场景下)。
务必确保 Body.Close() 在所有分支(包括 error 分支)中被执行:
resp, err := client.Do(req)
if err != nil {
// 即使出错,也要检查 resp 是否非 nil(超时可能返回 *http.Response + net.Error)
if resp != nil && resp.Body != nil {
resp.Body.Close() // 防止连接泄漏
}
return err
}
defer resp.Body.Close() // 正常路径
// 处理 resp...
真正复杂的地方在于:context 取消后,transport 可能还在尝试复用连接、重试或清理资源;Body.Close() 是告诉 transport “我不再需要这个响应流了”,这是释放资源的关键一步——很多人只记得 defer,却忘了 error 分支里也要关。
