真实接口耗时需绑定响应写出时刻,c.Next()前后打点会因Abort、panic、流式响应等漏计关键路径;必须用自定义ResponseWriter重写WriteHeader和Write,并确保注册在recovery之后全局生效。

为什么 c.Next() 前后打点会高估/低估真实耗时
真实接口耗时不是“中间件函数执行时间”,而是“从路由匹配完成到响应字节真正写出的时长”。c.Next() 只是触发 handler 链,但不保证它跑完——比如鉴权中间件调了 c.Abort(),后续 handler 根本没执行;或者业务 panic 后被 recovery() 拦住,状态码在 recovery 里才写,但你的计时早已结束。
常见错误现象包括:
-
c.Writer.Status()总是返回200或0,因为响应还没写出 - 流式接口(如 SSE、chunked)的耗时被严重低估,
Write没被调用就结束了计时 - 304、204 这类无 body 响应,
Write完全不触发,只靠WriteHeader才能捕获
必须用自定义 ResponseWriter 包装才能捕获真实写出时刻
GIN 的 c.Writer 是个接口,底层是 responseWriter。仅读 Status() 不够,它只返回已设置的状态码,不反映是否发出。要抓“响应开始写出”这一瞬间,得包装 http.ResponseWriter,重写 WriteHeader 和 Write:
-
WriteHeader是所有状态码(2xx/4xx/5xx)实际发出的唯一入口 -
Write被调用说明 body 开始写出,对 streaming 接口至关重要 - 不能只重写
Write:204、304 等响应不会调Write,必须靠WriteHeader记录
示例核心结构:
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)
}
中间件注册顺序直接影响统计完整性
统计中间件必须放在 recovery() 之后、其他业务中间件之前。否则 panic 场景下,recovery() 会先改写状态码和响应体,但你的统计中间件还没执行到写出阶段,拿到的仍是初始值。
- 错误顺序:
logger → stats → auth → recovery → handler→ panic →recovery写出 → 你的stats已退出 - 正确顺序:
logger → auth → recovery → stats → handler→ panic →recovery写出 →stats.WriteHeader被触发 - 还要确保它全局注册(
r.Use(...)),而不是只挂某组路由下,否则非匹配路径漏统计
高频调用下自定义 Writer 的内存与性能开销不可忽略
每次请求都 new 一个 *statsWriter,看似轻量,但在 QPS 上万时,小对象分配会抬高 GC 压力。实测未复用时,单请求多分配 1–2 次堆对象,P99 延迟上升约 8–12μs。
- 不要在
statsWriter里存*gin.Context或 request/response 引用,否则引发数据竞争 - 可配合
sync.Pool复用*statsWriter实例,注意 Reset 字段(尤其是written、statusCode、start) - 若服务已用
jsoniter.ConfigFastest+c.Data(),那统计中间件本身就成了链路里最重的一环,得盯紧它的分配行为
最易被忽略的是:WriteHeader 和 Write 的调用时机差异——304 响应只走前者,而大 JSON 流可能分多次调后者,统计逻辑必须覆盖这两种路径,否则指标失真。



















