判断请求是否真被 context 取消,关键是在 c.next() 后检查 ctx.err() == context.deadlineexceeded,而非依赖 c.writer.status();需用 context.withtimeout 包装上下文,显式传递 ctx 至子 goroutine,并分层设置超时时间以避免误杀或失效。

超时中间件里怎么判断请求真被 context 取消了
关键不是看 c.Writer.Status(),而是检查 ctx.Err()。Gin 的超时中间件必须用 context.WithTimeout 包装原始请求上下文,之后在 c.Next() 执行完再判断是否超时。如果只依赖响应状态码,会漏掉那些 panic 后已写 200 header 但实际没返回数据的 case。
常见错误是:在 c.Next() 前就检查 ctx.Err() —— 此时还没执行业务逻辑,ctx.Err() 还是 nil;或者用 c.Writer.Status() == 0 判断是否未响应,但某些 handler 主动调用 c.Status(200) 后再 panic,status 就不是 0 了。
- 必须在
c.Next()之后检查ctx.Err() == context.DeadlineExceeded - 不要用
c.AbortWithError()替代超时判断——它不触发ctx.Err(),也不影响 context 状态 - 如果 handler 内启了 goroutine,记得把
ctx显式传进去,并用select { case 监听取消
为什么 c.Abort() 必须紧跟在超时响应后调用
c.Abort() 的作用是终止中间件链,防止后续 handler 继续执行。不加它,即使你已经写了 c.JSON(http.StatusGatewayTimeout, ...),后面的中间件(比如日志、DB 连接池释放)仍会跑,可能造成资源泄漏或重复上报。
典型误用:只写 c.Status() + c.String() 就结束函数,没调 c.Abort()。结果是超时响应发出去了,但 c.Next() 之后注册的其他中间件照常执行,甚至可能 panic(比如尝试往已关闭的 response writer 写数据)。
- 超时分支里必须按顺序:设置响应 →
c.Abort()→ return(或确保无后续逻辑) -
c.Abort()不影响当前中间件内已执行的代码,只阻断后续中间件和 handler - 多个中间件嵌套时,
c.Abort()对所有后续中间件生效,不只是下一个
如何区分超时和其他 5xx 错误
不能靠 c.Writer.Status() >= 500 统一报警,因为超时(http.StatusGatewayTimeout)和服务器内部错误(http.StatusInternalServerError)语义不同,处理策略也该不同:前者可能是下游依赖慢,后者更可能是代码 bug 或资源耗尽。
推荐做法是在中间件里显式标记错误类型。比如定义一个 AppError 类型,带 Level 字段;超时中间件主动调用 c.Error(&AppError{Level: "timeout"}),而 recovery 中间件对 panic 生成 Level: "critical" 的 error。报警逻辑再按 Level 分流。
- 避免用字符串匹配 error message 判断超时,比如
strings.Contains(err.Error(), "timeout")—— 一旦 message 改写就失效 - 业务 handler 里不要自己调
c.Status(504),统一由超时中间件兜底,保证来源可追溯 - 如果用了第三方超时库(如
gin-timeout),确认它是否支持自定义 error 注入,否则无法和你的报警系统联动
超时时间设太短或太长会出什么问题
设太短(比如 100ms)会导致大量健康请求被误杀,尤其在高延迟网络或冷启动时;设太长(比如 30s)则失去保护意义,慢请求持续占着 goroutine 和连接,拖垮整个服务。
真实场景建议分层设置:对外 API 接口用 5s,内部服务调用用 2s,静态资源用 30s(但需配合 Nginx 的 proxy_read_timeout)。注意 Go 的 http.Server.ReadTimeout 是连接级,和单个请求的 context 超时无关,别混用。
- 测试时用
time.Sleep()模拟慢 handler,验证超时是否准时触发 - 上线后监控
ctx.Err()频次 + 平均耗时,动态调优 timeout 值 - 别在全局
router.Use()里套统一超时中间件——不同接口容忍度差异很大
ctx.Done() 的 goroutine 还在跑,可能继续写 DB 或发 HTTP 请求,变成“幽灵任务”。











