默认的http.handler不记录请求日志是因为go标准库设计上不内置日志逻辑,需用户按需自定义中间件包装handler并重写responsewriter以捕获状态码、耗时等关键字段。

为什么默认的 http.Handler 不记录请求日志
Go 标准库的 http.ServeMux 和直接传给 http.ListenAndServe 的 handler 都不会自动打印访问日志。这不是遗漏,而是设计取舍:日志格式、采样策略、敏感字段脱敏、结构化输出等需求因项目而异,不适合内置。
所以你需要自己封装一层,用「中间件」模式套在业务 handler 外面。核心就是实现一个接受 http.Handler 并返回新 http.Handler 的函数。
最简可用的请求日志中间件怎么写
关键不是“全功能”,而是先跑通链路、确认时间/状态码/路径能正确捕获。下面这个版本只依赖标准库,无第三方依赖:
func loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
// 包装 ResponseWriter 以捕获状态码和响应体大小
lw := &responseWriter{ResponseWriter: w, statusCode: http.StatusOK}
next.ServeHTTP(lw, r)
duration := time.Since(start)
log.Printf("%s %s %s %d %dms",
r.RemoteAddr,
r.Method,
r.URL.Path,
lw.statusCode,
duration.Milliseconds())
})
}
type responseWriter struct {
http.ResponseWriter
statusCode int
}
func (rw *responseWriter) WriteHeader(code int) {
rw.statusCode = code
rw.ResponseWriter.WriteHeader(code)
}
- 必须包装
http.ResponseWriter,否则拿不到真实返回的状态码(WriteHeader可能没被显式调用,底层会补) - 不要用
r.Header.Get("User-Agent")等直接读取,有些代理或客户端可能不带,容易 panic;加空值判断或用log.Printf的格式化能力兜底 - 别在中间件里做耗时操作(如写磁盘、发 HTTP 请求),否则阻塞整个请求链路
如何避免日志中间件干扰 panic 恢复和超时控制
如果你用了 recover 中间件或 context.WithTimeout,顺序很重要。日志中间件应该放在最外层(即最后被包装、最先执行),否则它看不到最终状态码或真实耗时。
- 错误顺序:
recover → timeout → logging → yourHandler→ 日志看到的是 recover 后的 500,不是原始 panic - 正确顺序:
logging → recover → timeout → yourHandler→ 日志记录真实处理结果 - 用
http.StripPrefix或路由分组时,确保中间件包裹的是最终 handler,而不是某个子路由的 handler 实例
生产环境要加什么才不算裸奔
上线前至少补这三项,否则排查问题时你会回来重写:
- 用
log/slog替代log.Printf,支持结构化字段:slog.Info("http request", "method", r.Method, "path", r.URL.Path, "status", lw.statusCode, "duration_ms", duration.Milliseconds()) - 对
r.URL.Query()和r.PostForm做采样或开关控制——全量打日志可能泄露 token、密码、手机号 - 加请求 ID:从
r.Header.Get("X-Request-ID")读,没有就生成一个,贯穿整个请求生命周期(包括下游调用),方便链路追踪
真正麻烦的从来不是写日志,是日志太多看不清、太少了找不到上下文、格式不统一导致 grep 不了——这些细节比中间件本身更花时间。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











