在Golang的Web开发中,HTTP请求日志记录是排查问题、监控接口性能的基础手段。借助标准库提供的接口,我们可以把日志逻辑从业务代码中剥离,通过中间件统一处理。

为什么用中间件记录日志
如果在每一个路由处理函数里都手写日志,不仅代码重复,而且容易遗漏。Golang的net/http包设计了http.Handler接口,只要实现一个包装函数,就能在请求前后插入逻辑。这种中间件模式让日志收集和业务逻辑完全解耦。
另外,中间件可以拿到完整的请求和响应信息。比如请求的URL、方法、远程地址,以及最终返回的状态码和耗时。把这些数据写成一行结构化日志,对后期用ELK或Loki分析非常友好。
基础日志中间件实现
下面用一个最简单的例子展示如何包装http.Handler。我们在请求进来时记下时间,在调用下一个处理器之后输出日志。注意标准库的http.ResponseWriter本身不暴露状态码,需要用自定义类型包装。
package main
import (
"log"
"net/http"
"time"
)
type statusRecorder struct {
http.ResponseWriter
status int
}
func (r *statusRecorder) WriteHeader(code int) {
r.status = code
r.ResponseWriter.WriteHeader(code)
}
func LoggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
start := time.Now()
recorder := &statusRecorder{ResponseWriter: w, status: http.StatusOK}
next.ServeHTTP(recorder, req)
duration := time.Since(start)
log.Printf("method=%s path=%s status=%d duration=%s remote=%s",
req.Method, req.URL.Path, recorder.status, duration, req.RemoteAddr)
})
}
func hello(w http.ResponseWriter, req *http.Request) {
w.Write([]byte("hello"))
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/hello", hello)
http.ListenAndServe(":8080", LoggingMiddleware(mux))
}
上面的代码定义了一个statusRecorder,它嵌入了原有的ResponseWriter,并覆盖了WriteHeader方法以捕获状态码。中间件在调用next.ServeHTTP前后分别计算时间,最后用log.Printf输出。这种方式零依赖,适合小型服务。
需要留意的是,如果业务处理器没有显式调用WriteHeader,默认状态码是200。我们在初始化recorder时把status设为http.StatusOK,就能正确处理这种情况。同时,远程地址直接取自req.RemoteAddr,在反向代理后可能拿到的是代理IP,生产环境应通过请求头如X-Forwarded-For获取真实客户端。
记录请求体与响应体
有些场景需要把请求参数或响应内容也记进日志,例如调试第三方回调。此时不能直接读取req.Body,因为读过之后业务处理器就拿不到了。正确做法是用io.TeeReader把 Body 复制一份。
package main
import (
"bytes"
"io"
"log"
"net/http"
)
func BodyLoggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
var buf bytes.Buffer
if req.Body != nil {
req.Body = io.NopCloser(io.TeeReader(req.Body, &buf))
}
next.ServeHTTP(w, req)
log.Printf("request body: %s", buf.String())
})
}
这里用io.TeeReader在读取Body时同步写入buffer,业务侧读取的还是原来的流。不过记录完整Body会消耗内存,且可能包含敏感信息,实际项目中应只记录必要字段或限制长度。
响应体记录更复杂,因为要包装ResponseWriter的Write方法。如果业务使用了分块传输或流式返回,缓存全部内容可能导致内存暴涨。一般建议仅对调试路由开启,并通过配置开关控制。
使用结构化日志库
标准库log包输出的是文本行,不便于机器解析。可以换成log/slog或zap,将字段写成JSON。下面用slog演示。
package main
import (
"log/slog"
"net/http"
"time"
)
func SlogMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
start := time.Now()
next.ServeHTTP(w, req)
slog.Info("http request",
"method", req.Method,
"path", req.URL.Path,
"duration", time.Since(start).String(),
)
})
}
slog是Go 1.21引入的结构化日志包,支持多种Handler。上面代码把请求信息作为键值对输出,配合JSON Handler可直接被日志系统采集。如果服务并发高,还可以加上trace_id,从上下文取出并写入日志,方便链路追踪。
在真实项目中,我们往往还会在中间件里加入panic恢复、限流计数等逻辑。日志中间件应保持轻量,只做记录不做阻断,避免因为日志组件故障影响主流程。通过合理封装,Golang实现HTTP请求日志记录既简单又灵活。