真正可用的耗时统计必须绑定到响应写出那一刻,需用自定义responsewriter重写writeheader和write捕获真实写出时刻,并确保注册在recovery之后、全局生效且线程安全。

直接用 c.Next() 前后打点统计耗时,会漏掉 panic、abort、重定向等关键路径,状态码常记成 0 或 200;真正可用的分级统计必须绑定到响应写出那一刻,靠自定义 ResponseWriter 捕获 WriteHeader 和 Write 的真实触发时机。
为什么 c.Next() 前后计时不适用于分级统计
分级统计(比如按 100ms 分桶)对时间精度和路径覆盖要求极高。而 c.Next() 只是调度控制权,并不反映响应是否真正发出:
-
c.Next()返回后,handler 可能已 panic,但此时c.Writer.Status()还是初始值200,Recovery()尚未写入 500 - 调用
c.Abort()后,后续 handler 不执行,但你的计时已结束,耗时被错误归入“成功路径” - 304、204 等无 body 响应,
Write()根本不触发,仅靠c.Next()结束时间无法判断响应是否完成 - 流式接口(如 SSE、chunked JSON)中,
c.Next()返回 ≠ 响应写出完毕,body 可能还在异步 flush
必须用自定义 ResponseWriter 捕获写出时刻
分级统计的起点不是中间件执行,而是 HTTP 响应头/体实际写入网络连接的那一瞬。Gin 的 c.Writer 是接口,需包装底层 http.ResponseWriter,重写两个方法:
-
WriteHeader(code int):所有状态码最终都经此发出,是分级统计的“状态锚点” -
Write([]byte):body 开始写出的唯一信号,对 streaming 接口必不可少
示例结构体(注意线程安全):
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.start = time.Now() // 精确到写出头这一刻才开始计时(排除前置中间件干扰)
w.ResponseWriter.WriteHeader(code)
}
func (w *statsWriter) Write(b []byte) (int, error) {
if !w.written {
w.statusCode = http.StatusOK
w.written = true
w.start = time.Now()
}
return w.ResponseWriter.Write(b)
}
如何在 Gin 中注册并保证统计生效
注册顺序和作用域直接影响统计完整性:
- 必须注册在
Recovery()之后——否则 panic 被 recover 拦截后,状态码写在 recovery 里,你的statsWriter已失效 - 必须全局注册(
r.Use(statsMiddleware)),不能只挂某组路由,否则健康检查、静态资源等路径漏统计 - 中间件内需替换
c.Writer:c.Writer = &statsWriter{ResponseWriter: c.Writer},且要确保不覆盖已有 writer 包装(可加类型断言判断) - 分级逻辑放在
WriteHeader或Write里,而非defer:例如if cost := time.Since(w.start); cost
分级维度容易被忽略的关键点
只按耗时分桶远远不够,真实微服务场景下必须叠加上下文维度:
- 路径需标准化:把
/user/123归为/user/:id,否则/order/999999和/order/1被算作不同指标 - 状态码必须取自
WriteHeader参数,而非c.Writer.Status()——后者在未写 header 时返回 0,且 recovery 后可能被覆盖 - 避免采样污染:高频低耗时接口(如
/health)若不做白名单绕过,会稀释 P99 统计;建议用c.Request.URL.Path显式跳过 - 微秒级统计需防时钟倒退:
time.Since(w.start)在容器环境可能因 NTP 调整出现负值,应加if cost
最隐蔽的坑是:多个中间件层层包装 c.Writer 时,若没做类型穿透(比如二次包装后丢失原始 statsWriter),分级统计会彻底失效——务必在每次包装前检查 c.Writer 是否已是自定义类型。











