
HTTP handler 中间件怎么加请求日志
Go 标准库 http.Handler 本身不带日志能力,得自己包一层。最直接的方式是写个中间件函数,把原始 http.Handler 包进去,在调用前/后打日志。
关键点不是“怎么打印”,而是“怎么拿到完整请求信息 + 响应状态码 + 耗时”。别直接在 handler.ServeHTTP 前后记时间——响应体可能没写完,ResponseWriter 的状态码也拿不到。
- 必须用自定义的
responseWriter类型,重写WriteHeader方法来捕获状态码 - 用
time.Now()记开始时间,defer里算耗时,确保无论 panic 还是正常返回都触发 - 注意
r.URL.Path和r.URL.RawQuery拼起来才是完整路径,否则丢查询参数
func loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
lw := &loggingResponseWriter{ResponseWriter: w, statusCode: 200}
next.ServeHTTP(lw, r)
log.Printf("[%s] %s %s %d %v",
r.Method,
r.URL.Path,
r.RemoteAddr,
lw.statusCode,
time.Since(start),
)
})
}
type loggingResponseWriter struct {
http.ResponseWriter
statusCode int
}
func (lw *loggingResponseWriter) WriteHeader(code int) {
lw.statusCode = code
lw.ResponseWriter.WriteHeader(code)
}
为什么用 http.ResponseWriter 接口包装而不是直接改 handler
因为标准库所有路由(http.ServeMux、gorilla/mux、gin.Engine)都只认 http.Handler 接口,而 http.ResponseWriter 是传进来的参数,你没法改它底层实现。强行类型断言或反射会破坏兼容性,且在 HTTP/2 或某些中间件(比如 gzip)下容易出错。
正确姿势是“装饰”它:让新写的 loggingResponseWriter 实现全部 http.ResponseWriter 方法(包括 Write、Hijack、Flush 等),再把未覆盖的方法委托给原对象。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
- 漏实现
Write:日志能打,但响应体发不出去 - 漏实现
Hijack:WebSocket 或长连接会 panic(错误信息:Hijack is not supported) - 用
struct{ http.ResponseWriter }匿名嵌入不能自动代理方法,必须显式实现
gin 框架里怎么加请求日志且不干扰原有日志格式
gin 自带 gin.Logger(),但它默认输出到控制台且格式固定,和你的业务日志混在一起很难过滤。想统一用 log.Printf 或结构化日志(如 zerolog),就得替换掉它。
gin 的 Use 方法支持任意 gin.HandlerFunc,你可以写一个和官方 Logger 行为一致但输出可控的版本:
- 从
c.Request取Method、Path、ClientIP() - 用
c.Writer.Status()拿状态码(它内部已做了WriteHeader捕获) - 用
c.Keys或自定义 context 字段存开始时间,避免闭包捕获导致并发问题 - 别在中间件里调
c.Next()后立刻读Status()—— 如果 handler panic,这个值还是 0
func GinLogging() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
c.Set("startTime", start)
c.Next()
status := c.Writer.Status()
latency := time.Since(start)
clientIP := c.ClientIP()
method := c.Request.Method
path := c.Request.URL.Path
log.Printf("[%s] %s %s %d %v", method, path, clientIP, status, latency)
}
}
记录请求体和响应体会不会拖慢接口或引发 panic
会,而且非常容易踩坑。标准 http.Request.Body 是单次读取流,读一次就 EOF;http.ResponseWriter 的响应体也不提供读取接口。强行读会导致后续 handler 拿不到数据,或 panic(http: response.WriteHeader on hijacked connection)。
- 要记录请求体:必须用
io.TeeReader把 body 流同时写进 buffer,再用 buffer 构造新*http.Request(注意重设ContentLength) - 要记录响应体:得用
io.Pipe或bytes.Buffer拦截Write,再把内容写回原ResponseWriter,性能损耗明显 - 生产环境强烈建议关掉请求/响应体记录,只留 method、path、status、latency、ip —— 这些足够定位 90% 的问题
- 如果真要体内容,加开关控制,且限制最大长度(比如只取前 1KB),避免大文件上传卡死
真正难的不是怎么记,是怎么在不破坏 HTTP 协议语义、不引入竞态、不拖垮性能的前提下,安全地拿到那些本不该暴露的数据。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










