golang的http库超时

1. Client 端

在作为客户端发送请求时,超时控制主要分两个层面:高层级的 http.Client 超时 和 低层级的 http.Transport 精细化控制。在大多数情况下,设置 http.Client.Timeout 就足够了,它可以有效地防止整个请求过程的无限期等待。

img

1.1 http.Client.Timeout

最简单,但有时太粗暴。这是最常用的设置,它控制包括连接、重定向(Redirects)以及读取响应体在内的整个请求生命周期的最大时长。

1
2
3
4
5
c := &http.Client{
Timeout: 10 * time.Second,
}
resp, err := c.Get("http://example.com")

  • 涵盖范围:Dial(拨号) -> TLS Handshake(握手) -> Request Headers/Body Send -> Response Headers Read -> Response Body Read。
  • 优点:简单,能防止请求无限挂起。缺点:对于需要处理长流(Streaming)响应的请求(如下载大文件),这个超时如果不小心设置短了,连接会在中途被截断。
  • 如果整个过程的累计时间超过 Timeout,请求会被强制取消,并返回 net/http: request canceled (Client.Timeout exceeded) 错误。

不需要依赖 Client 的全局配置:

  • 使用 context 是控制单个请求超时的最佳实践。
  • 当 ctx 超时或被 cancel 时,net/http 库会立即关闭底层的 TCP 连接,从而中断请求。
1
2
3
4
5
6
7
8
9
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

req, _ := http.NewRequest("GET", "http://example.com", nil)
// 将 Context 注入请求
req = req.WithContext(ctx)

client := &http.Client{}
resp, err := client.Do(req)

1.2 http.Transport

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
t := &http.Transport{
// 连接超时:包括 DNS 解析和 TCP 握手
DialContext: (&net.Dialer{
Timeout: 30 * time.Second, // 限制建立TCP连接的时间
KeepAlive: 30 * time.Second,
}).DialContext,


TLSHandshakeTimeout: 10 * time.Second, // 限制 TLS握手的时间

ResponseHeaderTimeout: 10 * time.Second, // 限制读取response header的时间, 注意:这不包括读取 Response Body 的时间


IdleConnTimeout: 90 * time.Second, // 限制空闲连接(Keep-Alive)在连接池中的存活时间
}

c := &http.Client{Transport: t}
  • ResponseHeaderTimeout:非常有用。如果你连上服务器了,发了请求,但服务器处理逻辑卡死迟迟不回 Header,这个参数就会生效。
  • 注意:Transport 中没有一个参数能直接限制 “ 发送 Request Body” 的时间。

2. Server 端

Server 端的超时设置更为关键,如果不设置,甚至可能导致 Slowloris 攻击(一种通过建立大量慢连接耗尽服务器资源的攻击)。服务器超时主要在 http.Server 结构体中配置。

1
2
3
4
5
6
7
srv := &http.Server{
ReadHeaderTimeout: 5 * time.Second,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 30 * time.Second,
Handler: myHandler,
}

file-20251202193327303

请求生命周期:
Accept -> Read Headers -> Read Body -> Handle (你的业务逻辑) -> Write Response

  1. ReadHeaderTimeout (非常重要)
    • 含义:从连接被 Accept 开始,到 Request Header 读取完毕的时间。
    • 作用:防御 Slowloris 攻击。如果攻击者建立 TCP 连接后,很慢很慢地发送 Header,没有这个超时,服务器就会一直等。强烈建议设置。
  2. ReadTimeout
    • 含义:涵盖了 ReadHeaderTimeout + 读取 Request Body 的时间。
    • 注意:如果你的 Handler 不读取 Body,或者 Body 很小,这个时间约等于读取 Header 的时间。但如果 Body 很大(如上传文件),这个时间必须设置得足够长,否则上传会中断。
  3. WriteTimeout
    • 含义:通常指从读取完 Request Header 结束开始,到 Response 写完为止。
    • 注意:这包括了 Handle 处理业务逻辑的时间。如果你的接口需要查询慢 SQL 耗时 5 秒,而 WriteTimeout 设为 3 秒,响应将无法成功写回,或者连接被提前断开。
  4. IdleTimeout
    • 含义:当开启 Keep-Alive 时,一个请求结束后,等待下一个请求到来的时间。如果没设置,默认复用 ReadTimeout 的值。

注意点:

  • WriteTimeout 虽然包含了 Handler 的处理时间,但它不会在 Handler 运行过久时中断 Handler 的执行(它只是在写回数据时报错)。如果你想硬性限制 Handler 的运行时间(例如:无论数据库查多久,3 秒必须返回给用户超时错误),你需要使用中间件 http.TimeoutHandler
  • 如果你想要 “ 真正 “ 取消任务(而不是仅仅超时输出),可以结合 context.WithTimeout 使用。
1
2
3
4
5
6
7
8
9
10
11
func main() {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
time.Sleep(5 * time.Second) // 模拟慢操作
w.Write([]byte("Hello"))
})

// 如果 handler 超过 2秒没返回,用户会收到 503 "My timeout message"
timeoutHandler := http.TimeoutHandler(handler, 2*time.Second, "My timeout message")

http.ListenAndServe(":8080", timeoutHandler)
}

3. 提问问题

底层原理

net.Conn 为 Deadline 提供了多个方法 Set[Read|Write]Deadline(time.Time)。Deadline 是一个绝对时间值,当到达这个时间的时候,所有的 I/O 操作都会失败,返回超时 (timeout) 错误。

Deadline 不是超时 (timeout)。一旦设置它们永久生效 (或者直到下一次调用 SetDeadline), 不管此时连接是否被使用和怎么用。所以如果想使用 SetDeadline 建立超时机制,你不得不每次在 Read/Write 操作之前调用它。

日常建议

  1. 永远不要在生产环境直接使用 http.Get("url")(因为它使用默认的 DefaultClient,即无超时)。请务必自定义 &http.Client{Timeout: ...}
  2. Server 端:务必设置 ReadHeaderTimeout 和 ReadTimeout,这是防止资源耗尽的第一道防线。
  3. 上传/下载场景:对于涉及大文件传输的 Client 或 Server,不要设置过短的全局 Timeout,应该利用 IdleConnTimeout 并结合业务逻辑手动控制,或者对 Body 的读写操作单独进行流控。
  4. 业务处理超时:使用 context.WithTimeout 或 http.TimeoutHandler 来控制业务逻辑的执行时长。

最佳实践

对于 Client 端,最佳实践的目标是:快速失败(Fail Fast)。如果服务器挂了或者网络不通,不要让你的 goroutine 卡几分钟。

  1. 必须要设超时:用 http.Client.Timeout 做兜底。
  2. 大文件下载特例:如果是下载文件,将 Client.Timeout 设为 0,仅依赖 Transport.ResponseHeaderTimeout(确保服务器开始响应了)和 context(用于手动取消)。
  3. Context:养成习惯,所有请求都 WithContext,这样上层业务取消时,HTTP 请求也会立即中断。

Server 端的最佳实践目标是:自我保护。防止因为客户端的慢连接(如 2G 网络)或恶意攻击(Slowloris)耗尽服务器资源。

  1. 即使什么都不设,也要设 ReadHeaderTimeout:这是防止慢速攻击的底线。
  2. 区分场景:
    • 通用 API 服务:设置 ReadTimeout 和 WriteTimeout
    • 上传/下载服务:ReadTimeout 和 WriteTimeout 建议设为 0 或非常大,完全依靠 TCP 层面的 KeepAlive 和业务逻辑中的流式读写超时控制。
  3. 业务超时控制:推荐在 Handler 内部第一行代码就 ctx := r.Context(),并在调用数据库或 RPC 时传入这个 ctx。如果客户端断开了(触发了 WriteTimeout 或用户关闭了浏览器),这个 ctx 会被 cancel,你能节省后续的计算资源。

4. 参考资料