直接用 log.println 打印请求日志会出问题,因为其无上下文隔离导致多请求日志混淆,且若中间件读取 r.body(如记录请求体),后续 handler 将因 body 已 eof 而无法读取。

为什么直接用 log.Println 打印请求日志会出问题
因为 Go 的 HTTP handler 是并发执行的,log.Println 默认不带上下文隔离,多个请求的日志会混在一起,看不出哪条日志属于哪个请求;更严重的是,如果在中间件里读取了 r.Body(比如想记录请求体),后续 handler 就再也读不到它了——r.Body 是一次性流,读完就 EOF。
如何安全地记录请求方法、路径、状态码和耗时
核心是包装 http.ResponseWriter,拦截 WriteHeader 调用以捕获状态码,并用 time.Since 计算耗时。不需要碰 r.Body 或 r.Header,这些字段本身是安全可读的。
- 用自定义结构体嵌入
http.ResponseWriter,重写WriteHeader方法,保存状态码并记录起始时间 - 避免在中间件里调用
r.ParseForm()或io.ReadAll(r.Body),除非你明确要消费并重新构造Body - 注意:如果 handler 主动 panic,状态码可能没被写入,此时应 fallback 到 500
type loggingResponseWriter struct {
http.ResponseWriter
statusCode int
start time.Time
}
func (w *loggingResponseWriter) WriteHeader(code int) {
w.statusCode = code
w.ResponseWriter.WriteHeader(code)
}
func LoggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
lw := &loggingResponseWriter{
ResponseWriter: w,
start: start,
}
next.ServeHTTP(lw, r)
log.Printf("%s %s %d %v", r.Method, r.URL.Path, lw.statusCode, time.Since(start))
})
}
要不要记录请求体和响应体?什么情况下可以开
记录请求/响应体不是默认选项,而是需显式开启的“高成本操作”。它会增加内存占用、延长延迟,并可能泄露敏感字段(如密码、token)。
- 只在调试环境开启,生产环境禁用
io.ReadAll类操作 - 若必须记录,先检查
r.ContentLength是否合理(比如),超限直接跳过 - 对响应体记录,必须用
io.TeeReader或双写器(如io.MultiWriter),不能简单替换ResponseWriter的Write方法——否则 gzip 等编码会被破坏 - JSON 请求体可尝试用
json.RawMessage解析后脱敏再记录,但别在中间件里做完整反序列化
为什么用 http.StripPrefix 后日志里的路径可能不对
因为 http.StripPrefix 修改的是 r.URL.Path,但它发生在 handler 链中靠后的位置。如果你的中间件放在 StripPrefix 外层,看到的还是原始路径;如果放在内层,则看到的是已剥离后的路径——这取决于注册顺序。
- 统一做法:在中间件里始终用
r.URL.RequestURI()获取原始请求路径(含 query),它不受StripPrefix影响 - 若需匹配路由逻辑,建议把日志中间件放在所有路径处理之后(即最内层),或配合
chi.Router等支持路由参数提取的框架使用 - 注意
r.RequestURI是原始字节,可能含非法字符;r.URL.EscapedPath()更安全但不含 query
defer 导致 panic 时耗时统计失效。这些点不改,日志看着全,查问题时照样抓瞎。golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











