c.Next()前后计时不能用于分布式场景,因其无法捕获真实响应写出时刻,导致与APM系统Span时间错位、状态码误判;必须用包装ResponseWriter在WriteHeader/Write时精确记录。

为什么 c.Next() 前后计时不能用于分布式场景
因为耗时统计目标不是“中间件执行多久”,而是“请求从路由匹配完成到响应真正写出的端到端延迟”——这在分布式链路中必须对齐服务端真实写出时刻,否则无法与客户端上报、APM 系统(如 Jaeger、SkyWalking)的 Span 时间对齐。用 c.Next() 前后打点会把 panic 拦截、重定向、流式响应未写出等路径全部漏掉,状态码也常读成 0 或 200,导致 trace 里出现大量 200 但实际是 500 或 302 的错位。
必须用自定义 ResponseWriter 捕获真实写出时刻
Gin 的 c.Writer 是接口,底层是 responseWriter,仅调 c.Writer.Status() 不代表响应已发出。真实写出只发生在两个入口:WriteHeader()(发状态码和 header)和 Write()(发 body)。所以要包装它:
-
WriteHeader()被调即表示响应开始发出,此时记录状态码并标记已写出 -
Write()被调说明 body 正在发送,对 streaming 接口(如 SSE、chunked)至关重要 - 注意:304、204 等无 body 响应不会触发
Write(),只走WriteHeader(),所以不能只监听Write
示例核心结构:
type statsWriter struct {
http.ResponseWriter
statusCode int
written bool
start time.Time
}
func (w *statsWriter) WriteHeader(code int) {
w.statusCode = code
w.written = true
w.ResponseWriter.WriteHeader(code)
}
func (w *statsWriter) Write(b []byte) (int, error) {
if !w.written {
w.statusCode = http.StatusOK
w.written = true
}
return w.ResponseWriter.Write(b)
}
中间件注册顺序决定 trace 完整性
统计中间件必须放在 gin.Recovery() 之后、其他业务中间件之前。否则:panic 会被 recovery 拦截并改写状态码,但你的计时早已结束;c.Abort() 跳过的 handler 也不会被计入,而分布式 trace 要求所有出口路径都覆盖。
- 正确顺序:
r.Use(gin.Recovery())→r.Use(statsMiddleware())→r.Use(authMiddleware()) - 绝对不要把统计中间件注册在
gin.Logger()之前——Logger 本身会读c.Writer.Status(),可能触发 header 写出,干扰你的时间点 - 若用了
gzip.Gzip(),统计中间件必须在它前面,否则测的是压缩耗时而非业务耗时
高精度时间戳需防时钟倒退和并发污染
分布式 trace 要求 sub-millisecond 级别精度,time.Now() 在容器或虚拟机中可能因 NTP 校正倒退,直接用 time.Since(start) 可能产出负值,破坏 span duration。
- 改用
time.Now().UnixNano()记录起点,差值用整数减法,避免浮点误差和负值风险 - 不要在中间件里做同步 I/O(如写本地文件、调用 HTTP client),会阻塞 goroutine,扭曲真实耗时
- 如果要注入 traceID 到日志或埋点,用
c.Set("trace_id", ...),确保下游所有中间件都能取到,而不是依赖局部变量
最易被忽略的一点:流式接口(如 text/event-stream)的“耗时”其实是首字节时间(TTFB),不是末字节时间。统计中间件必须在 WriteHeader() 时就结束计时,否则会一直挂起直到连接关闭。











