echo.MiddlewareFunc 是最直接入口,因其 func(echo.Context) error 类型天然适配请求-响应周期的耗时统计,可全覆盖 panic、超时等分支;需用 time.Now() 与 c.Response().Status() 配合获取真实耗时,避免用 ctx.Set/Get 传数据,应直接记录日志,并过滤 WebSocket 和重定向场景。

为什么 echo.MiddlewareFunc 是最直接的入口
因为 Echo 的中间件机制本质就是函数链式调用,echo.MiddlewareFunc 类型定义为 func(echo.Context) error,它天然适配日志、计时这类“包裹请求-响应周期”的需求。不建议绕路用 echo.HTTPErrorHandler 或自定义 echo.Router,那些是处理错误或路由逻辑的,和耗时统计无关。
常见错误是把计时逻辑写在 handler 内部——这会导致每个接口重复粘贴,且无法覆盖 panic、超时、提前 return 等分支;中间件才能确保从 ctx.Request() 到 ctx.Response().WriteHeader() 全路径覆盖。
如何用 time.Now() 和 ctx.Response().Status() 获取真实耗时
关键不是只记开始时间,而是必须在 response 已写入状态码后读取结束时间,否则可能拿到不准确的值(比如 gzip 压缩、write header 延迟等)。Echo 提供了 ctx.Response().Writer 的包装能力,但更轻量的做法是利用 ctx.Response().Status() 触发 header flush 的副作用来判断响应是否已发出。
- 在中间件开头调用
start := time.Now() - 用
next(c)执行后续链路(含 handler 和后续中间件) - 调用
c.Response().Status()—— 这会强制写出 status line,此时再取time.Since(start)才反映真实网络+处理耗时 - 注意:如果 handler panic,
next(c)会返回 error,但Status()仍可安全调用(返回 0,不影响计时)
ctx.Set() 和 ctx.Get() 不适合跨中间件传耗时数据
很多人想把耗时存在 ctx.Set("latency", d) 供日志中间件读取,但这是错的:Echo 的 context 生命周期短,且 Set/Get 是为 handler 内部传递参数设计的,不是中间件通信通道。一旦你加了多个中间件(比如 auth → latency → logger),顺序错乱或并发场景下极易读到旧值或 nil。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
立即学习“go语言免费学习笔记(深入)”;
正确做法是直接在当前中间件里完成记录动作:
- 用
log.Printf或结构化日志库(如zerolog)直接输出 - 如果要用字段打点,把耗时、path、method、status 封装进一个 map,一次性传给日志函数
- 避免在中间件里做耗时操作(如写文件、HTTP 请求),否则统计本身会污染结果
别忽略 ctx.IsWebSocket() 和重定向场景
WebSocket 连接建立后不走 HTTP 响应流程,Response().Status() 始终为 0,直接计时会误报成“超长耗时”;301/302 重定向时,Status() 是 3xx,但实际业务逻辑可能极短——这两类都需要显式过滤。
- 开头加判断:
if ctx.IsWebSocket() { return next(c) } - 对重定向,可选是否记录(通常不记),或单独标记:
if status >= 300 && status - 更稳妥的是按
ctx.Request().Method+ctx.Request().URL.Path白名单控制统计范围,避免健康检查接口(如/health)刷屏日志
真正难的不是写完中间件,而是让耗时数字可信:它得包含网络传输、gzip、header 写入,又不能被日志 IO 拖慢,还得避开 WebSocket 和流式响应的陷阱。这些边界情况不处理,统计结果就只是个安慰剂。

















