如何在Golang中实现HTTP请求日志记录

来源:建站技术作者:缅甸程序员头衔:程序员
导读:本期聚焦于小伙伴创作的《如何在Golang中实现HTTP请求日志记录》,敬请观看详情。想把每次HTTP请求的耗时、状态码和客户端信息都记下来,却不想在每个处理函数里重复写日志代码?利用Golang的http.Handler中间件机制,可以把日志记录逻辑集中到一处。通过包装原有的处理器,在请求进入时记录开始时间,在响应结束后计算耗时并输出结构化日志。相比在业务代码中散落日志语句,中间件方式更利于维护,也方便后续接入日志收集系统。本文演示如何用标准库net/http实现轻量日志中间件,并说明如何避免常见并发与响应体读取陷阱。

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

如何在Golang中实现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请求日志记录既简单又灵活。

GolangHTTP中间件请求日志修改时间:2026-08-09 07:33:27

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。