Gin中间件中time.Now()需在c.Next()前调用并存入上下文,否则会包含路由匹配等非handler耗时;应使用time.Since()、自定义responseWriter捕获状态码与响应大小,并将耗时中间件置于鉴权等前置中间件之后、handler之前。

为什么直接用 time.Now() 记开始时间会不准
因为 Gin 中间件的执行时机在路由匹配之后、handler 执行之前,但如果你把 time.Now() 放在中间件函数入口,它会把路由查找、中间件自身初始化、甚至前置全局中间件(比如鉴权)的时间也计入——这不是你关心的“接口真实耗时”。
- 正确做法是:在
c.Next()前调用time.Now(),并存到上下文:c.Set("start_time", time.Now()) - 必须用
c.Get("start_time")取,不能依赖闭包变量——并发请求下会互相覆盖 - 别用
time.Now().Sub(t),改用time.Since(t),它基于单调时钟,不受系统时间调整影响
gin.Logger() 的耗时字段不能直接当接口耗时用
gin.Logger() 默认输出的耗时(比如 [GIN] 2026/08/11 - 18:57:00 | 200 | 12.345ms)是从中间件入口开始算的,不是 handler 实际执行时间。压测时你会发现这个值比 Prometheus 抓到的 http_request_duration_seconds 高 2–5ms,差的就是日志格式化、锁 stdout、写入缓冲这些开销。
- 如果要做监控指标上报,必须自己记
start_time并在c.Next()后计算 - 不要复用
log.Printf打耗时——它锁stdout,QPS 上千就成瓶颈 - 推荐用
zerolog或zap写结构化日志,或把耗时发到 metrics channel 异步聚合
怎么拿到准确的状态码和响应体大小
状态码不能靠 c.Writer.Status() 在 c.Next() 后读——它只在 WriteHeader 被调用后才有效;而有些 handler(比如返回 204 No Content)根本不调 Write,只调 WriteHeader。响应体大小更难,c.Writer.Size() 对 gzip 压缩后的长度不敏感,且无法区分 header 和 body。
Colly 是一个用于 Go 语言的快速开源爬取和爬虫框架。它适用于从简单的页面提取到异步爬虫处理大量页面集合,支持请求回调和结构化解析。
- 必须包装
http.ResponseWriter,实现自己的responseWriter类型 - 重写
WriteHeader(int)和Write([]byte)方法,在里面更新状态码和字节数 - 注意:304、204 等无 body 响应,
Write不会被调用,所以状态码必须从WriteHeader捕获 - 每个请求 new 一个实例,别复用——否则并发下计数错乱
中间件注册顺序影响统计结果
如果你把耗时中间件放在鉴权中间件后面,那统计里就包含了鉴权逻辑;如果放在最外层,又会包含 Gin 自身的路由匹配开销。实际生产中,多数人想要的是“handler 执行耗时”,所以它应该紧贴 handler,也就是放在所有前置校验中间件之后、handler 之前。
立即学习“go语言免费学习笔记(深入)”;
- 全局注册时用
r.Use(authMiddleware, statsMiddleware),而不是r.Use(statsMiddleware, authMiddleware) - 对特定路由组单独加:
apiV1 := r.Group("/v1"); apiV1.Use(statsMiddleware); apiV1.GET("/user", userHandler) - 别在
gin.Default()创建的 router 上直接用——它的默认中间件(如 recovery)会干扰统计,改用gin.New()+ 显式注册
真实耗时统计的关键不在代码多寡,而在 timing point 的选择和 responseWriter 的可控性。很多人卡在“为什么指标和 APM 工具对不上”,问题往往出在没隔离 handler 执行阶段,或者用了不可靠的状态码读取方式。

















