gin 的 r.static() 默认不发 cache-control 头,因其底层使用 http.fileserver,仅设置 content-type 和 last-modified,导致浏览器和 cdn 无法强缓存;需在 r.static() 前注册中间件拦截 /static/ 路径并手动添加 "public, max-age=31536000, immutable" 头,且 index.html 必须单独设 no-cache。

Gin 的 r.Static() 默认不发 Cache-Control 头,浏览器不会强缓存,必须手动加中间件注入头 —— 这不是漏配,是 Gin 原生行为。
为什么直接写 r.Static("/static", "./dist/static") 不生效
因为底层用的是 http.FileServer,它只设 Content-Type 和 Last-Modified,完全不写缓存头。浏览器收到响应后按默认策略(通常是 no-cache 或短时间)处理,CDN 也因缺失 Cache-Control 跳过缓存。你看到的“没缓存”不是路径错、不是权限问题,就是头根本没发。
- 检查响应头:用浏览器 DevTools → Network → 点开一个
.js文件,看 Response Headers 里有没有Cache-Control - 常见错误现象:
200 OK但每次刷新都重拉 JS/CSS;CDN 回源率 100% -
gin.StaticOptions{MaxAge: 31536000}在 v1.9+ 才支持,旧版本会静默忽略 —— 别盲目套用
必须在 r.Static() 前注册中间件拦截 /static/
中间件顺序错了就完全不生效。Gin 的 r.Static() 是路由级 handler,一旦匹配成功,后续中间件收不到请求。所以缓存头必须在它之前塞进去。
- 路径判断要用
strings.HasPrefix(c.Request.URL.Path, "/static/"),注意结尾斜杠是否一致(比如你注册的是/static,但请求是/static/,也要覆盖) - 别写成
c.Request.URL.Path == "/static",这会漏掉所有子文件 - 中间件里调用
c.Header("Cache-Control", "public, max-age=31536000, immutable"),immutable很关键,能跳过浏览器的 If-None-Match 验证 - 确保
c.Next()在头之后调用,否则可能被后续逻辑覆盖
index.html 必须单独禁用长缓存
JS/CSS 可以缓存一年,但 index.html 绝对不能。它一缓存,用户就永远看不到新版本入口,哪怕 JS 已更新也会报错。
- 不要把
index.html放进/static/目录再走同一套缓存逻辑 - 根路由
r.GET("/", ...)中手动加c.Header("Cache-Control", "no-cache"),再调用c.File("./dist/index.html") - 如果用了
embed.FS,得用r.StaticFS("/static", http.FS(assets)),但index.html仍要走独立 handler - 更稳妥的做法:把
index.html放进/static/下,用中间件对/static/index.html单独覆盖头:if c.Request.URL.Path == "/static/index.html" { c.Header("Cache-Control", "no-cache") }
强缓存真正起效的三个隐藏前提
写了头 ≠ 浏览器真缓存。很多线上问题卡在这三步上,本地测通不代表生产可用。
- 文件必须返回
200 OK:路径错、权限不足、Windows 下反斜杠未转filepath.ToSlash(),都会导致 404 或 500,头写了也白写 - 构建产物必须带内容哈希:如
app.a1b2c3.js,否则缓存再久,更新后用户还是跑旧 JS - CDN 或代理层没 strip 头:某些企业网关或老旧 CDN 会过滤
Cache-Control,需在边缘侧确认响应头是否透传











