
本文介绍一种无需修改业务逻辑、基于中间件链的优雅方式,通过包装 `http.responsewriter` 实现对生产环境 http 响应的完整日志记录(含状态码、header 和响应体),并提供可复用的 `newresponselogginghandler` 实现。
在 go 的 HTTP 服务中,直接从 http.ResponseWriter 获取已写入的响应体(如 jsON)和完整 Header 是受限的——因为 ResponseWriter 是一个接口,不暴露底层缓冲区。httputil.DumpResponse 只接受 *http.Response,而它通常只在客户端侧可用;服务端的响应尚未序列化为 *http.Response 对象,因此不能直接复用。
解决这一问题的核心思路是:在请求处理链中插入一个“响应拦截层”,用 httptest.ResponseRecorder 替代原始 ResponseWriter,让后续处理器向 recorder 写入响应,再由该层统一读取、记录,并透传回真实响应流。
以下是一个轻量、无第三方依赖的实现方案(兼容标准库 net/http):
package main import ( "fmt" "io" "log" "net/http" "net/http/httptest" "strings" ) // NewResponseLoggingHandler 是一个 HTTP 处理器组合子(middleware) // 它将原始 handler 包装,记录其生成的完整响应(状态码、Header、Body) func NewResponseLoggingHandler(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // 1. 创建 recorder 拦截响应 rec := httptest.NewRecorder() // 2. 调用下游 handler(写入的是 rec,而非原始 w) next(rec, r) // 3. 记录响应信息(可根据需要写入文件、日志系统等) log.Printf("→ %s %s %d", r.Method, r.URL.Path, rec.Code) log.Printf("Headers: %+v", rec.HeaderMap) if strings.Contains(rec.Header().Get("Content-Type"), "application/json") && rec.Body.Len() > 0 { log.printf("Body (json): %s", rec.Body.String()) } // 4. 将 recorder 中的内容复制到真实 ResponseWriter for k, vs := range rec.HeaderMap { for _, v := range vs { w.Header().Add(k, v) } } w.WriteHeader(rec.Code) rec.Body.WriteTo(w) // 注意:WriteTo 自动处理 io.Copy,安全高效 } } // 示例业务处理器(返回 JSON) func exampleHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) fmt.Fprint(w, `{"message":"Hello, World!","status":"ok"}`) } func main() { // 构建带日志能力的路由 mux := http.NewServeMux() mux.HandleFunc("/api/hello", NewResponseLoggingHandler(exampleHandler)) log.Println("Server starting on :8080...") log.Fatal(http.ListenAndServe(":8080", mux)) }
✅ 关键优势说明:
- 零侵入性:业务 handler 完全无需修改,仍接收标准 http.ResponseWriter;
- 生产就绪:httptest.ResponseRecorder 是标准库组件,非测试专用,线程安全且无副作用;
- 精准控制:可在记录前检查 Content-Type、响应长度或状态码,避免大文件/敏感数据全量打印;
- 可组合性强:可与其他中间件(如认证、CORS、监控)自由叠加,例如:
h := NewAuthMiddleware(NewResponseLoggingHandler(NewMetricsHandler(exampleHandler)))
⚠️ 注意事项:
- rec.Body.String() 会将整个响应体加载到内存,若响应体极大(如文件下载),建议改用 io.CopyN 截断日志或跳过 Body 记录;
- 若 handler 中调用了 w.(http.Hijacker).Hijack()(如 websocket 升级),则不可使用此方案(因 hijack 后 ResponseWriter 不再写入 body);
- 日志输出请接入结构化日志系统(如 zap 或 zerolog),避免 log.Printf 在高并发下成为瓶颈。
通过这种函数式中间件设计,你不仅能轻松实现响应日志,还能构建可复用、易测试、易扩展的 HTTP 处理链——这才是 Go 生态中“小而美”工程实践的典范。