bigywater.tech
Gin · 50

超时控制:http.TimeoutHandler 与 context 的对比

请求超时有三种常见做法:TimeoutHandler、context 超时、http.Server 的各种 Timeout。它们对客户端和 handler 的效果完全不同。这篇用一个 300 ms 的慢 handler 和 100 ms 的超时,把三种方式的响应、handler 是否被打断、以及 r.Run 默认没有任何超时这件事一一实测。

103 ms vs 303 ms
TimeoutHandler 下客户端收到 503 的时间 vs handler 实际结束的时间
EOF
WriteTimeout 到点后客户端拿到的结果:没有响应,连接断开
1501 ms
r.RunListener 默认下,发了一半请求头的连接等了这么久也没被关闭

TimeoutHandler:只管客户端

实测 · 本机执行输出
       1 ms handler 开始
     103 ms 客户端收到 503 "超时了"(耗时 103 ms)err=<nil>
     303 ms handler 睡醒,Request.Context().Err()=context deadline exceeded
     303 ms handler 写响应的返回值 err=http: Handler timeout

http.TimeoutHandler 到点向客户端回 503,但 handler 在另一个 goroutine 里继续跑完:数据库查询照常执行、副作用照常发生,最后写出的内容被丢弃(返回 http.ErrHandlerTimeout)。它给的是客户端体验上的超时,不是资源上的超时。

context:要配合

实测 · 本机执行输出
       1 ms handler 开始
     102 ms handler 收到取消:context deadline exceeded,立即返回 504
     102 ms handler 返回
     102 ms 客户端收到 504 "{\"error\":\"timeout\"}"(耗时 102 ms)err=<nil>

给 Request 换上带超时的 context,handler 里所有可能阻塞的调用(数据库、HTTP 客户端、select)都接受它,超时才能真正中止工作。不接受 context 的调用不受任何影响。这种方式能返回你自己定义的 504 响应。

不检查 context 的 handler

实测 · 本机执行输出
    客户端收到 200 "睡了 300ms,ctx.Err()=context deadline exceeded"(耗时 303 ms)err=<nil>

context 只是一个取消信号,不会打断正在执行的代码。handler 里如果调用的是不接受 context 的慢函数(这里用 time.Sleep 代替),超时对它毫无作用,客户端仍要等满 300 ms。很多“设置了超时却没生效”的问题都是因为中间某一层没有把 context 传下去。

WriteTimeout 的体验

实测 · 本机执行输出
    WriteTimeout=100ms:客户端 0 "" 耗时 301 ms,错误:EOF
    TimeoutHandler=100ms:客户端 503 "超时了" 耗时 102 ms,错误:<nil>

WriteTimeout 是连接级的写截止时间,到点时不会返回任何响应,而是在写出时失败、关闭连接:客户端要等到 handler 结束才看到 EOF。它适合作为兜底,不适合当“请求超时”用。

r.Run 没有任何超时

实测 · 本机执行输出
  gin.go 的 RunListener(Gin v1.12.0):
     645 | func (engine *Engine) RunListener(listener net.Listener) (err error) {
     646 |     debugPrint("Listening and serving HTTP on listener what's bind with address@%s", listener.Addr())
     647 |     defer func() { debugPrintError(err) }()
     648 | 
     649 |     if engine.isUnsafeTrustedProxies() {
     650 |         debugPrint("[WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.\n" +
     651 |             "Please check https://github.com/gin-gonic/gin/blob/master/docs/doc.md#dont-trust-all-proxies for details.")
     652 |     }
     653 | 
     654 |     server := &http.Server{ // #nosec G112
     655 |         Handler: engine.Handler(),
     656 |     }
     657 |     err = server.Serve(listener)
     658 |     return
     659 | }
    r.RunListener(默认):等了 1501 ms,服务端一直没有关闭连接(请求头还没发完)
    http.Server{ReadHeaderTimeout: 300ms}:300 ms 后连接被服务端关闭:读到 0 字节 "" err=EOF

r.Run / RunListener 内部的 http.Server 只设了 Handler(源码里连 #nosec G112 的静态检查提示都标了)。慢速发送请求头的连接会被一直保持。生产环境应该自己创建 http.Server,至少设置 ReadHeaderTimeout,再按需要设置 ReadTimeout、WriteTimeout、IdleTimeout,然后 srv.ListenAndServe()。

分步演示

超时控制
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Go 1.27.1(brew)/ Gin v1.12.0(go.mod 里 require,源码在模块缓存里);用 go run / go test 直接编译运行,需要库时经代理从 proxy.golang.org 下载;HTTP 实验用 httptest 或本机回环端口(9700 以上);计时用 time.Now / testing.B,数字每次运行会有小幅抖动。服务端是真实的 httptest.NewServer(WriteTimeout 与 ReadHeaderTimeout 的实验给 Config 设置对应字段);慢速请求头实验用原始 TCP 连接只发半个请求头;耗时是 time.Now 差,每种情形各一次(数字有几毫秒抖动)
    • 没有验证:TimeoutHandler 与 Gin 中间件(如 Logger、Recovery)组合时日志的内容;被超时的请求在 handler 里 panic 的行为;ReadTimeout、IdleTimeout 的行为(只实验了 WriteTimeout 与 ReadHeaderTimeout);HTTP/2 下的超时;gin-contrib/timeout 等第三方库
    京ICP备2026067502号-1京公网安备11010502063487号