gin 默认logger()不建议用于生产环境,因其日志格式固定、不支持结构化输出、无法动态添加字段、级别混杂且硬编码写入os.stdout;推荐用loggerwithwriter()重定向输出,或手写中间件+zap实现高性能结构化日志。

为什么 Gin 默认的 Logger() 中间件不建议直接用于生产环境
因为它的输出是固定格式、硬编码到中间件内部的,无法控制字段顺序、不支持结构化(如 JSON)、不能动态添加请求 ID 或用户信息,且日志级别混杂(比如把 404 当 error 打出来)。更关键的是:它直接写到 os.Stdout,没法对接 logrus/zap 等主流日志库。
用 LoggerWithWriter() 替换默认日志中间件并重定向输出
这是最轻量级的定制方式——保持 Gin 原有日志逻辑,只换输出目标和格式化行为。核心是传入自定义的 io.Writer 实现,让每条日志先被拦截再加工。
- 用
gin.DefaultWriter可以统一接管所有日志输出(包括错误和访问日志) - 实现一个带缓冲的
io.Writer,在Write([]byte)方法里解析原始日志行(Gin 的默认日志是空格分隔的字符串,如"[GIN] 2024/03/15 - 10:22:33 | 200 | 1.234ms | 127.0.0.1 | GET /api/user") - 用正则提取时间、状态码、耗时、路径等字段,再按需拼接为 JSON 或添加 trace_id 字段
- 注意:不要在
Write里做耗时操作(如网络写入),否则会阻塞 HTTP 请求处理
logger := &customWriter{writer: os.Stderr}
gin.DefaultWriter = logger
r := gin.New()
r.Use(gin.LoggerWithWriter(logger)) // 注意这里仍要显式调用
完全绕过 Gin 日志中间件,用 gin.HandlerFunc 自己写请求日志
当需要深度控制(比如只记录特定 method/path、跳过健康检查、绑定 context 中的用户 ID)时,手写中间件更可靠。Gin 的 c.Next() 调用前后可精确获取请求开始/结束时间、状态码、响应体长度等。
- 从
c.Request.Context()获取或生成trace_id(推荐用middleware.TraceID()或reqid库注入) - 用
c.Errors捕获中间件 panic(但 Gin 不会把 handler panic 自动加进这里,需配合Recovery()) - 状态码必须在
c.Writer.Status()中读取(不能用c.Writer.Written()判断是否已写,因为 Header 可能已发) - 避免在日志中打印完整
c.Request.Body(已读过就不可重放,影响后续中间件)
func AccessLog() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
c.Next()
latency := time.Since(start)
status := c.Writer.Status()
method := c.Request.Method
path := c.Request.URL.Path
ip := c.ClientIP()
userAgent := c.GetHeader("User-Agent")
// 输出结构化日志(例如用 zap.Sugar().Infof)
log.Printf("%s %s %d %s %s %s %v", method, path, status, ip, userAgent, traceIDFromCtx(c), latency)
}
}
用 zap + gin-contrib/zap 做高性能结构化日志
这是生产环境最推荐的方式:利用 zap 的零分配日志能力,配合 gin-contrib/zap 提供的中间件,天然支持字段追加、采样、异步写入。
- 安装:
go get github.com/gin-contrib/zap - 初始化
zap.Logger后,传给zap.GinLogger(zapLogger)和zap.RecoveryWithZap(zapLogger, true) - 通过
c.Set("logger", sugared.With(...))把带上下文字段的 logger 注入 context,在下游 handler 中复用 - 注意:
zap.GinLogger默认不记录请求 body 和 response body,如需记录需自己扩展(但通常不建议,有性能和隐私风险)
真正容易被忽略的是:zap 的 AtomicLevel 在热更新日志级别时,Gin 中间件不会自动感知——你得自己封装一层 wrapper,在每次日志前检查当前 level 是否允许输出。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











