
本文介绍一种无需修改业务逻辑、基于中间件链的优雅方式,通过包装 `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 生态中“小而美”工程实践的典范。










