
本文介绍如何在 Go 的 http.ListenAndServe 中拦截并记录 HTTP 响应状态码与响应体,通过自定义 ResponseWriter 实现无侵入式日志记录,解决原生 http.ResponseWriter 不支持直接读取响应数据的问题。
本文介绍如何在 go 的 `http.listenandserve` 中拦截并记录 http 响应状态码与响应体,通过自定义 `responsewriter` 实现无侵入式日志记录,解决原生 `http.responsewriter` 不支持直接读取响应数据的问题。
在 Go 的标准 HTTP 服务器中,http.Handler 接收的 http.ResponseWriter 是一个接口,仅提供写入响应的能力(如 WriteHeader 和 Write),并不暴露已写入的内容或最终状态码——这使得直接调用 httputil.DumpResponse 不可行,因为该函数需要一个可读取的 *http.Response 实例。
为实现响应日志,核心思路是包装(wrap)ResponseWriter:创建一个结构体嵌入原始 ResponseWriter,重写其 WriteHeader 和 Write 方法,在转发调用前捕获状态码和响应体。以下是一个生产就绪的轻量级实现:
package main
import (
"log"
"net/http"
)
func sampleHandler(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("Hello, World!"))
}
type loggingResponseWriter struct {
status int
body []byte // 推荐保留为 []byte,避免 UTF-8 截断或编码丢失
http.ResponseWriter
}
func (w *loggingResponseWriter) WriteHeader(statusCode int) {
w.status = statusCode
w.ResponseWriter.WriteHeader(statusCode)
}
func (w *loggingResponseWriter) Write(b []byte) (int, error) {
w.body = append(w.body[:0], b...) // 安全拷贝,避免后续修改影响日志
return w.ResponseWriter.Write(b)
}
func responseLogger(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
lrw := &loggingResponseWriter{
ResponseWriter: w,
status: http.StatusOK, // 默认 200,WriteHeader 调用后更新
}
h.ServeHTTP(lrw, r)
log.Printf("REQ=%s %s | STATUS=%d | BODY_LEN=%d", r.Method, r.URL.Path, lrw.status, len(lrw.body))
})
}
func main() {
http.Handle("/", responseLogger(http.HandlerFunc(sampleHandler)))
log.Println("Server starting on :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
✅ 关键设计说明:
- body 字段使用 []byte 而非 string,规避因非法 UTF-8 字节导致的日志截断或 panic;
- Write 中采用 append(w.body[:0], b...) 实现零分配拷贝(slice reset + 复制),兼顾性能与安全性;
- status 初始化为 http.StatusOK,确保未显式调用 WriteHeader 时仍能输出合理默认值;
- 日志格式包含请求方法、路径、状态码及响应体长度——对调试与监控足够有效,如需完整响应体,可按需启用(注意敏感信息脱敏)。
⚠️ 注意事项:
- 此方案不适用于流式响应(如 chunked 编码)或大文件下载场景,因需缓冲全部响应体到内存;
- 若 Handler 中多次调用 Write,当前实现仅记录最后一次内容(常见于模板渲染等场景),如需完整拼接,可将 Write 改为追加模式(w.body = append(w.body, b...));
- 生产环境建议结合结构化日志库(如 zap)并添加请求 ID 关联,便于链路追踪;
- 避免在 WriteHeader 或 Write 中执行耗时操作,否则会阻塞 HTTP 连接。
通过该包装器,你可在不修改业务逻辑的前提下,统一为任意 Handler 添加响应日志能力,是 Go HTTP 中间件设计的经典范式之一。











