必须手动过滤 gin.Logger() 的静态文件日志,因其无状态、不感知路由顺序,会在 Static 处理前就记录;可用 LoggerWithConfig 跳过精确路径,或自定义中间件用 strings.HasPrefix 过滤前缀路径。

默认的 gin.Logger() 会记录所有请求,包括 /assets/xxx.js、/favicon.ico 这类静态文件访问——这不仅污染日志,还会在高并发下拖慢响应。必须手动过滤。
为什么 gin.Logger() 无法直接排除静态路径
因为 gin.Logger() 是一个无状态中间件,它不感知路由注册顺序,也不检查请求是否命中了 r.Static() 路由。它只管“来了就记”,哪怕后续被 Static 处理并短路返回,日志也已打出。
常见错误现象:curl /assets/main.css 后,日志里仍出现 [GIN] ... | 200 | ... | GET "/assets/main.css",但你根本没写这个 handler。
根本原因:Gin 的 Static 是通过内部路由匹配 + 文件系统读取实现的,不是靠普通 GET handler 拦截;而 Logger() 在整个中间件链最前端,早于任何路由分发逻辑。
用 gin.LoggerWithConfig() + 自定义 SkipPaths
官方提供了跳过路径的机制,但仅支持完全匹配(不是前缀或通配),所以适用于明确知道的单个文件:
-
favicon.ico、robots.txt、manifest.json这类固定路径可以直接加进SkipPaths - 不要试图把
/assets加进去——它不会匹配/assets/css/app.css - 该配置对性能无影响,判断是字符串比较,在日志写入前完成
示例:
r.Use(gin.LoggerWithConfig(gin.LoggerConfig{
SkipPaths: []string{"/favicon.ico", "/robots.txt"},
}))
更实用的方案:自定义 Logger 中间件 + 路径前缀判断
当你要屏蔽整个 /assets、/static 或 /images 目录时,必须自己写中间件,用 strings.HasPrefix(c.Request.URL.Path, "/assets") 判断:
- 放在
r.Use()中,且必须在gin.Recovery()之前(否则 panic 时拿不到 request path) - 调用
c.Next()前做判断,不满足条件才执行日志逻辑 - 注意:如果用了
r.StaticFS()或嵌入文件系统(embed.FS),路径匹配逻辑不变,仍看Request.URL.Path
简短示例:
func FilteredLogger() gin.HandlerFunc {
return func(c *gin.Context) {
path := c.Request.URL.Path
if strings.HasPrefix(path, "/assets/") || path == "/favicon.ico" {
c.Next() // 不打日志,直接往后走
return
}
// 否则走原日志逻辑
start := time.Now()
c.Next()
latency := time.Since(start)
status := c.Writer.Status()
method := c.Request.Method
clientIP := c.ClientIP()
log.Printf("[GIN] %s | %d | %v | %s | %s",
method, status, latency, clientIP, path)
}
}
// 使用
r.Use(FilteredLogger())
别忽略:静态文件中间件本身的日志干扰
如果你额外引入了 gin-contrib/static,它的 static.Serve() 默认不打日志——这点比原生 r.Static() 干净。但一旦你给它加了 wrapper 或自定义 http.FileSystem,就要确认里面没偷偷调 log.Print。
最容易被忽略的点:开发时用 air 热重载,air.conf 的 exclude_dir 里如果漏了 assets,会导致每次改 CSS 都触发构建,间接放大日志量——这和 Logger 无关,但会让“日志太多”的问题更难定位。


















