★ 超时控制:服务端/客户端超时怎么设
结论先行
超时要分三层设,缺一不可:① 传输层 http.Server 的 ReadTimeout/WriteTimeout/ReadHeaderTimeout/IdleTimeout,防慢连接拖死进程;② 请求上下文层 context.WithTimeout,控制每个 handler 内下游调用的截止时间;③ 客户端层 http.Client.Timeout / 请求级 ctx,防止自己的调用方挂死。Gin 本身不内置 handler 超时中间件,需要自己用 context 组合。
分层配置表
| 层 | 手段 | 防什么 |
|---|---|---|
| Server 读 | ReadTimeout、ReadHeaderTimeout | Slowloris、慢上传 |
| Server 写 | WriteTimeout | handler 慢/响应写不完 |
| Server 空闲 | IdleTimeout | keep-alive 空连接占资源 |
| Handler 内 | context.WithTimeout(c.Request.Context(), …) 传给 DB/HTTP/Redis | 单个下游卡死 |
| Client | http.Client{Timeout}、NewRequestWithContext | 自己发起的调用无限等待 |
代码示例
// 1) 服务端传输层
srv := &http.Server{
Addr: ":8080",
Handler: r,
ReadHeaderTimeout: 5 * time.Second, // 必须设:防慢请求头攻击
ReadTimeout: 10 * time.Second, // 含读 body
WriteTimeout: 15 * time.Second, // 覆盖 handler + 写响应
IdleTimeout: 60 * time.Second,
}
// 2) handler 内下游调用:把超时传给每一个 io
ctx, cancel := context.WithTimeout(c.Request.Context(), 2*time.Second)
defer cancel()
rows, err := db.QueryContext(ctx, "SELECT ...") // 超时自动取消
// 3) 客户端
client := &http.Client{Timeout: 5 * time.Second} // 整请求总超时
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
resp, err := client.Do(req)
细节与坑
- http.Server 各项默认是 0 = 无限,不配置就没有任何保护;Go 官方也建议至少设置 ReadHeaderTimeout。
- WriteTimeout 的起止语义与版本相关:它从请求头读完开始覆盖到响应写完,间接覆盖了 handler 执行期(HTTP/1.1),精确语义以官方文档为准。
- 超时到 ≠ handler 被杀:context 取消只让“认 ctx 的下游调用”失败;若 handler 在死循环且不检查 ctx,仍需 WriteTimeout 兜底断开。
- 客户端总超时与细分超时:Transport.DialContext/TLSHandshakeTimeout/ResponseHeaderTimeout 控制建立连接的环节;总超时是兜底。
- 生产还要给反向代理(Nginx/网关)配超时,且服务端各超时值应小于上游,形成“上游更宽松”的链条。
常见追问 / 记忆点
- 追问:为什么只设 http.Server 超时还不够?答:它管“连接与写响应”,管不了 handler 内部对 DB 的调用是否泄漏 goroutine——那要靠 context 传递。
- 追问:Gin 有没有现成超时中间件?答:官方不带;社区方案本质都是把 c.Request 换成带 deadline 的 ctx 再放行。
- 记忆点:三件套——Server 传输超时 + ctx 取消链 + Client 超时;默认全是 0,等于裸奔。