真实接口耗时应从路由 handler 开始前计起,需在 c.Next() 前存 time.Now() 到上下文,之后用 c.Writer.Status() 获取准确状态码,并将统计中间件置于 Recovery 之后以覆盖 panic 场景。

为什么直接用 c.Next() 前后计时会不准
因为 c.Next() 之前执行的代码,可能包含鉴权、路径匹配、中间件前置逻辑等非业务耗时;c.Next() 返回后读取的 c.Writer.Status() 也可能被 recovery 中间件覆盖或未及时刷新。真实接口耗时应从路由 handler 开始执行前算起,而不是中间件入口。
常见错误是把整个中间件生命周期当“接口耗时”,压测时看到平均 120ms,实际业务逻辑只占 35ms,其余全是日志写入、token 解析、context 拷贝开销。
- 必须在
c.Next()前调用time.Now(),并存入c上下文:c.Set("start_time", time.Now()) - 在
c.Next()后取值:start, _ := c.Get("start_time"),再用time.Since(start.(time.Time)) - 别用闭包变量存 start 时间——并发请求会互相覆盖
- 避免用
log.Printf实时打点,它锁 stdout,QPS 过千时成瓶颈
gin.Logger() 不能直接用于统计平均响应时间
默认 gin.Logger() 输出的是“中间件执行耗时”,不是接口真实响应时间;且它不区分 404/500 等状态码,所有失败请求都记为 200 或 0;更严重的是,它把 query 参数原样打印,含 token 或密码字段时存在安全风险。
生产环境必须替换为自定义中间件,并显式捕获 c.Writer.Status()(注意:Gin v1.9+ 才保证该值准确,旧版本需升级)。
立即学习“go语言免费学习笔记(深入)”;
- 不要依赖
c.Status()—— 它只是设置值,不反映实际写出的状态码 - 必须用
c.Writer.Status(),这是底层ResponseWriter实际写入 header 后的值 - 若 handler panic,
Recovery会设 status=500,但发生在 logger 之后 → 把自定义统计中间件放在Recovery之后 - 对 abort 场景(如鉴权失败),要主动调用
c.AbortWithStatus(401),否则Status()保持初始 0
如何安全聚合耗时并计算平均值
高频接口每秒数百请求,用 atomic.AddInt64 累加总耗时 + 总请求数,比用 mutex 锁 map 更轻量;但要注意:纳秒级 time.Duration 直接累加易溢出,必须转为毫秒再存。
示例关键逻辑:
var (
totalDuration int64
totalCount int64
)
func StatsMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
c.Set("start_time", start)
c.Next()
dur := time.Since(start).Milliseconds()
atomic.AddInt64(&totalDuration, int64(dur))
atomic.AddInt64(&totalCount, 1)
}
}
- 暴露
/stats接口时,用atomic.LoadInt64读取,避免读写竞争 - 计算平均值:如果
totalCount == 0,返回 0,别除零 panic - 别用 float64 做原子操作——Go 不支持,先 load 再算
- 高并发下,单个
int64计数器够用;但若需按 path/method 维度拆分,得用sync.Map或预分配 map + 读写锁
统计中间件必须放在 Recovery 之后
这是最容易被忽略的顺序问题。Gin 默认 gin.Default() 的中间件顺序是 Logger → Recovery,而 Recovery 在 panic 时会强制写入 500 状态码并终止后续写入。如果你的统计中间件在 Recovery 之前,那么 panic 请求的耗时会被记录,但状态码仍是 0 或 200,导致平均耗时虚高、错误率漏报。
正确注册方式:
r := gin.New()
r.Use(gin.Recovery()) // 先兜底
r.Use(StatsMiddleware()) // 再统计
r.Use(yourAuthMiddleware())
r.GET("/api/user", handler)
- 全局中间件顺序决定执行链,
r.Use()调用越早,越靠近请求入口 - 统计类中间件应紧贴 Recovery 后,确保所有路径(包括 panic 路径)都被捕获
- 不要把统计中间件和 auth/logic 中间件混在同一层——职责分离,便于调试和开关
- 如果用了路由组,确保组内
Use()也遵循该顺序
c.Writer.Status() 和对应纳秒级 time.Since() 的严格配对。中间件位置错一位,指标就失真;状态码读错一个字段,告警就失效。


















