中间件中通过在next.servehttp()前记录start := time.now()、用defer计算time.since(start).milliseconds()获取毫秒级耗时,并用responsewriterwrapper劫持writeheader/write以准确记录状态码和响应字节数。

中间件里怎么拿到请求开始和结束时间
Go 的 HTTP 中间件本质是函数套函数,http.Handler 或 http.HandlerFunc 类型的包装器。关键在于:请求进入中间件时记录起始时间,调用 next.ServeHTTP() 后记录结束时间。不能用 time.Now().Unix() 这种整数秒级精度——得用 time.Now() 返回的 time.Time 值,再用 .Sub() 算纳秒级耗时。
常见错误是把 time.Now() 放在 next.ServeHTTP() 之后才调,那样测的是中间件自身执行时间,不是整个请求耗时。
实操建议:
- 在
next.ServeHTTP()前调用start := time.Now() - 用
defer在函数返回前记录耗时(确保无论是否 panic 都能打点) - 耗时建议转成毫秒:
elapsed := time.Since(start).Milliseconds(),避免直接打印纳秒值造成日志过长
如何把耗时写进日志且不干扰原响应
Go 标准库的 http.ResponseWriter 是接口,不能直接读取状态码或响应体长度。要记录状态码和字节数,必须用一个包装器(wrapper)实现 http.ResponseWriter 接口,劫持 WriteHeader() 和 Write() 调用。
容易踩的坑:
- 只重写
Write()忽略WriteHeader(),导致状态码永远记成 200 - 没透传
Header()方法,某些中间件(比如 CORS)会失效 - 响应体很大时,反复
Write()导致多次计数,应只在Write()第一次调用后累加长度
最小可用 wrapper 示例:
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
type responseWriterWrapper struct {
http.ResponseWriter
statusCode int
written int
}
<p>func (w *responseWriterWrapper) WriteHeader(code int) {
w.statusCode = code
w.ResponseWriter.WriteHeader(code)
}</p><p>func (w *responseWriterWrapper) Write(b []byte) (int, error) {
if w.statusCode == 0 {
w.statusCode = http.StatusOK
}
n, err := w.ResponseWriter.Write(b)
w.written += n
return n, err
}</p>
Gin / Echo / Fiber 框架里怎么插这个中间件
各主流框架注册方式不同,但核心逻辑一致:把耗时记录逻辑包进框架要求的中间件签名里,并确保它在路由匹配之后、处理器执行之前被调用。
参数差异和注意点:
- Gin:
gin.HandlerFunc,需用c.Writer获取响应包装器,c.Next()替代next.ServeHTTP() - Echo:
echo.MiddlewareFunc,用c.Response().Status和c.Response().Size可直接读状态码和字节数,无需手动 wrapper(但要注意Size在c.Next()后才准确) - Fiber:
fiber.Handler,c.Response().StatusCode()和c.Response().Length()同样可用,但需确认是否已 flush;若启用压缩,Length()是压缩后大小
性能影响:只要不往日志里塞请求体或大 header,纯计时 + 小量字段写入(方法、路径、状态码、毫秒数)基本无感。但若开启 DEBUG 日志并每秒上千请求,I/O 可能成为瓶颈,建议异步写日志或采样输出。
为什么本地测出来耗时总是 0ms 或 1ms
这不是代码 bug,而是 Go HTTP server 默认启用 keep-alive,复用 TCP 连接,且本地回环(127.0.0.1)延迟极低。真实耗时要看三类时间:
- 网络传输时间(客户端到 server 的 RTT)
- 服务端处理时间(你中间件测的这部分)
- 响应写出时间(特别是流式响应或大文件)
验证方法:
- 用
curl -w "@format.txt" http://localhost:8080/加自定义 format 输出服务端时间 - 在中间件里加
log.Printf("path=%s, status=%d, elapsed=%.2fms", r.URL.Path, w.statusCode, elapsed),然后用 ab / wrk 压测 - 故意在 handler 里
time.Sleep(100 * time.Millisecond)看日志是否反映出来
真正容易被忽略的是:如果 handler panic 了,而你的 defer 日志没 recover,那这条耗时日志就根本不会打出——务必在中间件最外层加 defer func() { if r := recover(); r != nil { /* 记录 panic + 耗时 */ } }()。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










