★ 超时控制:服务端/客户端超时怎么设

结论先行

超时要分三层设,缺一不可:① 传输层 http.Server 的 ReadTimeout/WriteTimeout/ReadHeaderTimeout/IdleTimeout,防慢连接拖死进程;② 请求上下文层 context.WithTimeout,控制每个 handler 内下游调用的截止时间;③ 客户端层 http.Client.Timeout / 请求级 ctx,防止自己的调用方挂死。Gin 本身不内置 handler 超时中间件,需要自己用 context 组合。

分层配置表

手段防什么
Server 读ReadTimeout、ReadHeaderTimeoutSlowloris、慢上传
Server 写WriteTimeouthandler 慢/响应写不完
Server 空闲IdleTimeoutkeep-alive 空连接占资源
Handler 内context.WithTimeout(c.Request.Context(), …) 传给 DB/HTTP/Redis单个下游卡死
Clienthttp.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,等于裸奔。
笔记加载中…