真实延迟必须绑定响应写出时刻,因c.Next()返回不等于响应发出;需包装ResponseWriter重写WriteHeader和Write捕获状态码与body写出点,且stats中间件须注册在recovery之后。

响应延迟不能只看 c.Next() 前后打点,真实耗时必须绑定到响应写出那一刻,否则 90% 的统计值不可信。
为什么 c.Next() 计时会严重失真
很多人在中间件里写 start := time.Now() → c.Next() → log.Printf("latency: %v", time.Since(start)),但这根本不是接口真实延迟。
- 若 handler panic 被
recovery()拦住,c.Writer.Status()还是 200,但实际状态码是 500,且写出发生在 recovery 里 - 若某个鉴权中间件调了
c.Abort(),c.Next()后的逻辑根本没执行,但你的计时已结束 - 304、204 这类无 body 响应,
Write()根本不触发,仅靠c.Next()返回时间无法捕获写出时刻 - 流式响应(如 SSE)或异步写场景下,
c.Next()返回时 body 可能还没开始发
必须用自定义 ResponseWriter 捕获真实写出点
真实延迟 = 请求匹配完成 → 状态码和 header 发出(WriteHeader)或 body 开始写出(Write)的时间差。只有包装 http.ResponseWriter 才能精确钩住这两个入口。
-
WriteHeader(code int)是所有状态码最终落定的唯一位置,包括 304/401/500 -
Write([]byte)是 body 实际发出的起点,对 chunked 或 streaming 接口至关重要 - 注意:不要只重写
Write,否则 204/304 会被漏掉;必须同时覆盖WriteHeader - 示例中
statsWriter用written bool标记是否已触发写出,避免重复计时
中间件注册顺序决定你能不能拿到准确数据
自定义 ResponseWriter 中间件必须注册在 recovery() 之后、其他业务中间件之前,否则 recovery 会绕过你的包装直接写原生 writer。
- 错误顺序:
r.Use(logger)→r.Use(recovery)→r.Use(stats)→r.GET(...)(stats 拿不到 recovery 写出的数据) - 正确顺序:
r.Use(recovery)→r.Use(stats)→r.Use(auth)→r.Use(logger)→r.GET(...) - 全局注册才有效;按路由组局部注册容易漏掉 404 或 OPTIONS 预检请求
- 务必确保
stats中间件线程安全——*gin.Context不跨 goroutine 复用,但statsWriter实例需每次新建
延迟数据要关联关键上下文才可定位瓶颈
光有毫秒数没用,得知道这个延迟发生在哪里。真实生产环境里,必须把耗时和以下字段一起打点:
-
c.Request.Method+c.Request.URL.Path:确认是不是特定路径退化 -
c.ClientIP()+c.GetHeader("User-Agent"):排查客户端异常或爬虫刷量 -
c.Writer.Status():区分是业务慢(200 耗时高)还是错误多(5xx 集中爆发) - 手动注入的 trace ID(如从 header 读
X-Trace-ID):对接 SkyWalking 或 Jaeger 做链路下钻 - 避免在中间件里做耗时操作(如同步写磁盘日志),否则统计本身就成了瓶颈
最常被忽略的是:写出时刻不等于 handler 返回时刻,也不等于 c.Next() 返回时刻。只要没真正调用底层 TCP write,延迟就还在累积。哪怕你优化了 JSON 序列化、去掉了中间件,如果统计方式错了,问题依然看不见。


















