请求超时有三种常见做法:TimeoutHandler、context 超时、http.Server 的各种 Timeout。它们对客户端和 handler 的效果完全不同。这篇用一个 300 ms 的慢 handler 和 100 ms 的超时,把三种方式的响应、handler 是否被打断、以及 r.Run 默认没有任何超时这件事一一实测。
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 timeouthttp.TimeoutHandler 到点向客户端回 503,但 handler 在另一个 goroutine 里继续跑完:数据库查询照常执行、副作用照常发生,最后写出的内容被丢弃(返回 http.ErrHandlerTimeout)。它给的是客户端体验上的超时,不是资源上的超时。
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 响应。
客户端收到 200 "睡了 300ms,ctx.Err()=context deadline exceeded"(耗时 303 ms)err=<nil>
context 只是一个取消信号,不会打断正在执行的代码。handler 里如果调用的是不接受 context 的慢函数(这里用 time.Sleep 代替),超时对它毫无作用,客户端仍要等满 300 ms。很多“设置了超时却没生效”的问题都是因为中间某一层没有把 context 传下去。
WriteTimeout=100ms:客户端 0 "" 耗时 301 ms,错误:EOF
TimeoutHandler=100ms:客户端 503 "超时了" 耗时 102 ms,错误:<nil>WriteTimeout 是连接级的写截止时间,到点时不会返回任何响应,而是在写出时失败、关闭连接:客户端要等到 handler 结束才看到 EOF。它适合作为兜底,不适合当“请求超时”用。
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=EOFr.Run / RunListener 内部的 http.Server 只设了 Handler(源码里连 #nosec G112 的静态检查提示都标了)。慢速发送请求头的连接会被一直保持。生产环境应该自己创建 http.Server,至少设置 ReadHeaderTimeout,再按需要设置 ReadTimeout、WriteTimeout、IdleTimeout,然后 srv.ListenAndServe()。