必须在next.ServeHTTP返回后立即用time.Since()计算耗时,而非defer;需包装ResponseWriter重写WriteHeader和Write以准确捕获状态码和字节数,避免隐式200或重复写头。

Go HTTP 中间件怎么记录 handler 执行耗时
直接用 http.HandlerFunc 包裹原始 handler,在调用前后记录时间差即可。关键不是“怎么记”,而是“记准”——必须在 handler.ServeHTTP 调用完成后再计算耗时,否则拿到的是调度延迟而非真实处理时间。
常见错误是把 time.Now() 放在 handler.ServeHTTP 之前就 defer 记录,结果统计包含写响应头、刷 buffer 等后续操作,数值偏高且不稳定。
- 正确做法:在
handler.ServeHTTP返回后立刻调用time.Since() - 别用
defer记录耗时,defer 在函数 return 前执行,但此时 response 可能还没 flush - 如果用了
ResponseWriter的包装(比如记录 status code),务必确保包装器的WriteHeader和Write方法不干扰计时逻辑
用 http.ResponseWriter 包装器获取状态码和字节数
单纯耗时不够,生产环境需要关联 status 和 bytes written。Go 标准库没有内置 ResponseWriter 包装类型,得自己实现一个轻量 wrapper。
重点在于重写 WriteHeader 和 Write 方法,同时避免重复写 header 或 double write panic。
-
WriteHeader只能调一次,wrapper 要用statusCode字段标记是否已写 -
Write方法里累加bytesWritten,但注意:如果 handler 没显式调WriteHeader,Write第一次调用会隐式写200 OK - 不要在 wrapper 里调
wrapped.WriteHeader(),除非确定 status 未设;否则可能触发http: multiple response.WriteHeader calls
中间件如何注入到 Gin / Echo / net/http 路由链
不同框架注册方式差异大,但核心都是“把原始 handler 传给中间件函数,返回新 handler”。别硬套模板,看框架文档对 middleware 的签名要求。
Gin 的中间件接收 *gin.Context,Echo 是 echo.HandlerFunc,而原生 net/http 是 http.Handler ——三者不能混用。
- Gin:
func(c *gin.Context) { c.Next(); /* 耗时逻辑放这里 */ },注意c.Next()是同步阻塞调用,计时可放其后 - Echo:
func(next echo.HandlerFunc) echo.HandlerFunc,返回的 handler 内要显式调next(c) - net/http:
func(http.Handler) http.Handler,返回的 handler 调用h.ServeHTTP(w, r),耗时统计放这行之后 - 别在 Gin/Echo 中直接用
http.Handler类型中间件,类型不匹配会 panic
为什么响应时间统计常比真实慢 1–5ms
不是代码问题,而是 Go runtime 调度和网络栈行为导致的。尤其在高并发下,goroutine 被抢占、TCP ACK 延迟、客户端接收缓冲区满等都会让 Write 返回变慢,这部分被计入“响应时间”,但实际业务逻辑早已结束。
如果要做精准性能分析,建议只统计到 handler 函数退出为止(即业务逻辑完成点),而不是 Write 返回。但线上监控通常以 client 感知为准,所以保持当前方式更贴近真实体验。
真正容易被忽略的是:日志打点或 metrics 上报本身有开销,尤其用 log.Printf 或未缓存的 Prometheus Observe(),可能让平均耗时虚高 0.2–1ms —— 高 QPS 下必须用异步或批处理上报。

















