gin.logger() 无法自定义格式,因其内部硬编码日志模板且不暴露配置接口;需改用 gin.loggerwithformatter 或手写中间件,结合 context 取字段、c.writer.status() 获取状态码、time.since() 计算耗时,并推荐 zerolog/zap 实现高性能结构化日志。

直接说结论:gin.Logger() 不能改格式,想自定义必须自己写中间件,用 gin.LoggerWithFormatter 或手动拼接 log.Printf + c.Writer.Status() 等字段。
gin.Logger() 为什么不能改输出格式
它内部硬编码了日志模板字符串,不暴露格式化入口。调用 gin.Logger() 就是固定格式,比如 2024/05/12 14:22:33 | 200 | 1.245µs | 127.0.0.1 | GET "/api/users",没法增删字段或调整顺序。
常见错误是以为加个配置就能改——其实 Gin 没提供类似 SetFormat() 这样的方法。想加 trace_id、user_id、请求体长度,或者把耗时单位从 µs 换成 ms,都得绕开它。
-
gin.Logger()是只读的、不可扩展的,适合快速验证,不适合生产定制 - 它不捕获 panic,也不记录响应体,更不会自动注入 context.Value 中的字段
- 所有字段(状态码、耗时、IP)都是在
c.Next()后硬取的,你没法提前干预拼接逻辑
用 gin.LoggerWithFormatter 自定义格式
这是 Gin 官方提供的“可插拔”方案,本质是传一个函数,由你决定每条日志长什么样。核心是实现 gin.LogFormatter 类型,接收 *gin.LogFormatterParams 参数。
示例:输出 JSON 格式,含 trace_id(从 context 取)、method、path、status、latency:
func CustomLogFormatter(param gin.LogFormatterParams) string {
traceID := "unknown"
if tid, ok := param.Context.Get("trace_id"); ok {
traceID = tid.(string)
}
return fmt.Sprintf(`{"trace_id":"%s","method":"%s","path":"%s","status":%d,"latency_ms":%.3f,"ip":"%s"}\n`,
traceID,
param.Method,
param.Path,
param.StatusCode,
float64(param.Latency.Microseconds())/1000.0,
param.ClientIP,
)
}
// 使用
r.Use(gin.LoggerWithFormatter(CustomLogFormatter))
-
param.Latency是time.Duration,单位是纳秒,别直接用.Seconds()——小数点后太多位,建议转成毫秒并保留三位小数 -
param.ClientIP在反向代理后可能不准,需配合gin.ForwardedByClientIP = true和 X-Forwarded-For 头使用 - 返回字符串末尾要加
\n,否则多条日志会挤在同一行
手写中间件:更灵活但要注意生命周期
如果 gin.LoggerWithFormatter 不够用(比如想异步写日志、或需要访问 response body),就得自己写 gin.HandlerFunc。关键点是:耗时要在 c.Next() 后算,状态码要从 c.Writer.Status() 取,不能用 c.Writer.Status() 之前调用的值。
示例:记录 IP、method、path、status、耗时,并打上 level 字段:
func AccessLog() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
c.Next()
latency := time.Since(start)
status := c.Writer.Status()
clientIP := c.ClientIP()
method := c.Request.Method
path := c.Request.URL.Path
log.Printf("[ACCESS] %s %s %d %v %s", method, path, status, latency, clientIP)
}
}
- 不要在
c.Next()前读c.Writer.Status()——此时还是 0,永远是 200 - 如果用了
c.Abort()(如鉴权失败),c.Next()不会执行后续 handler,但中间件仍会继续往下走,所以c.Writer.Status()能拿到真实状态码 - 避免在中间件里做阻塞操作(如写文件、HTTP 请求),高并发下会拖慢整个请求链路;真要异步,用 goroutine + channel,但注意
c不能跨 goroutine 直接用,得用c.Copy()
结构化日志和性能陷阱
纯字符串拼接(如 log.Printf)在高频请求下有明显性能损耗:格式化字符串、内存分配、锁竞争。真正生产环境推荐用 zerolog 或 zap 替代标准 log。
例如用 zerolog 写中间件:
logger := zerolog.New(os.Stdout).With().Timestamp().Logger()
// … 在中间件里
logger.Info().
Str("method", c.Request.Method).
Str("path", c.Request.URL.Path).
Int("status", c.Writer.Status()).
Dur("latency", time.Since(start)).
Str("ip", c.ClientIP()).
Msg("http_access")
- zerolog 的
Msg()不触发格式化,而是延迟序列化,比fmt.Sprintf快得多 - 字段名必须是常量字符串,动态 key(如
logger.Str(key, value))会破坏结构化能力,慎用 - 别在中间件里反复 new logger 实例——复用一个全局 logger,否则 GC 压力大
最易被忽略的一点:无论用哪种方式,trace_id、user_id 这类上下文字段,必须显式从 c.Request.Context() 里取,而不是依赖中间件执行顺序“自动带过去”。Gin 的 context 是 request-scoped,但不会自动注入到日志库的 field 中——你得亲手塞。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











