登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go教程

Go net/http 怎样为静态文件设置缓存头

来源:17golang原创

时间:2026-11-02 23:54:29 145浏览 收藏

用 Go 的 net/http 提供前端静态文件时,真正决定浏览器和 CDN 缓存多久的不是 http.FileServer 这个名字,而是响应里的 Cache-Control。最稳妥的做法是在 FileServer 外面包一层 handler:带内容 hash 的 JS、CSS、字体可以长时间缓存,入口 HTML 则保留重新确认的机会。

官方文档:https://pkg.go.dev/net/http

这篇只讨论静态文件由 Go 直接提供的情况,不把缓存头、文件修改时间和 CDN 刷新混成一件事。下面的示例把策略写在服务端,资源文件仍交给标准库处理。

先把缓存策略分成两类

我在给一个前后端一体的小服务加缓存时,最先遇到的不是 API,而是“首页更新了,浏览器却还显示旧内容”。原因通常是把所有静态文件都设置成同一个长缓存。HTML 入口可能在每次发布时变化,而带 hash 的资源名本身已经承担了版本标识,二者的缓存策略应该分开。

静态资源请求按 HTML 入口和带 hash 资源区分并连接到 Cache-Control、浏览器缓存和 CDN 缓存的技术结构图
图1:缓存策略关系说明图,展示 HTML 入口与带 hash 资源的缓存边界,不是运行截图。
资源常见响应头适用原因
带内容 hash 的 JS、CSS、字体public, max-age=31536000, immutable文件名变化就代表内容版本变化
没有 hash 的 HTML 入口no-cache 或较短的 max-age发布后希望客户端重新确认
用户私有或临时文件按业务需要选择 private 或 no-store不应被共享缓存复用

no-cache 不是“不允许保存”,而是要求复用前向源站确认;如果响应内容根本不能落盘或不能被再次使用,才考虑 no-store。因此不要为了“保险”把所有静态资源都写成 no-store。

用 handler 包住 FileServer

http.FileServer 返回一个标准 handler,适合直接挂到路由上。缓存头属于应用对资源发布策略的判断,可以放在委托前完成;文件打开、内容类型、修改时间、范围请求等工作仍由标准库负责。

package main

import (
    "log"
    "net/http"
    "strings"
)

func cachedStatic(root http.FileSystem) http.Handler {
    files := http.FileServer(root)

    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // 带 hash 的构建产物可以长缓存;入口 HTML 必须允许重新确认。
        if strings.HasPrefix(r.URL.Path, "/assets/") {
            w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")
        } else {
            w.Header().Set("Cache-Control", "no-cache")
        }

        // 继续交给 FileServer 处理路径、文件内容和标准 HTTP 响应。
        files.ServeHTTP(w, r)
    })
}

func main() {
    // 静态目录下的 assets/ 与 index.html 由同一个服务提供。
    handler := cachedStatic(http.Dir("./public"))
    log.Fatal(http.ListenAndServe(":8080", handler))
}

这个包装器的关键点只有两个:先写 ResponseWriter.Header(),再调用底层 handler;同时把“资源是否带版本”落实为稳定的路径或命名约定。示例使用 /assets/ 作为约定,真实项目也可以在构建阶段给文件名加 hash 后统一放入该目录。

http.Handler、缓存包装器、ResponseWriter.Header、http.FileServer、http.ServeContent 和 Last-Modified 的静态关系图
图2:FileServer 与缓存包装器的静态关系图,展示响应头写入与标准库文件服务的分工。

路径判断要和资源命名保持一致

最简单的判断是看 URL 前缀,但它只在路由约定稳定时可靠。不要仅凭“扩展名像图片”就给所有请求一年缓存:同一个后缀可能来自用户上传目录,也可能是需要马上替换的业务文件。

func staticPolicy(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // 只对构建产物目录使用 immutable,避免误伤用户文件。
        isAsset := strings.HasPrefix(r.URL.Path, "/assets/")
        if isAsset {
            w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")
        } else if r.URL.Path == "/" || strings.HasSuffix(r.URL.Path, ".html") {
            // HTML 入口允许缓存,但每次复用前都要回源确认。
            w.Header().Set("Cache-Control", "no-cache")
        }

        // 不在包装层重新读取文件,保留下游 handler 的错误处理。
        next.ServeHTTP(w, r)
    })
}

如果服务前还有反向代理或 CDN,要把这套分类同步到它们的缓存规则中。源站发送了 public,并不等于 CDN 一定会按期望缓存;代理仍可能受到自身规则、响应状态和认证信息影响。

不要把缓存头和条件请求混为一谈

Go 的文件服务会根据文件信息参与常见的 HTTP 条件处理,文档中也列出了 ServeContent、ServeFile 和 FileServer 等接口。它们解决的是“内容怎样被提供、修改时间和范围怎样参与响应”;Cache-Control 解决的是“客户端和共享缓存能否保存、保存多久、是否要重新确认”。设置前者不会自动替你决定后者。

  • Last-Modified / 条件请求:帮助客户端判断已有副本是否仍可复用,适合降低重复传输。
  • Cache-Control:描述缓存权限和时间,例如 public、max-age、immutable。
  • Range:服务大文件或媒体片段时表达部分内容请求,和缓存时长是不同维度。

所以,给 FileServer 加 Cache-Control 后,不能据此断言“以后一定不会请求源站”,也不能把一次 304 当成缓存策略已经正确。发布系统、代理层和资源命名仍要配合。

上线前按发布方式选择参数

可以把最后的决策压缩成三问:

  1. 文件名变化是否能可靠表示内容变化?能,就考虑长缓存和 immutable;不能,就使用短缓存或 no-cache。
  2. 资源是否可能含用户私有内容?是,就不要直接使用面向共享缓存的 public。
  3. 中间是否有 CDN 或反向代理?有,就把源站头和代理规则一起设计,明确谁负责失效。

对多数 Go 前后端一体服务,一个可维护的默认值是:构建产物放进独立的 /assets/ 目录并带 hash,使用长缓存;HTML 入口使用 no-cache;不确定归属的上传文件不要套用静态资源策略。这样既能减少重复下载,也不至于让一次前端发布长期卡在旧入口上。

相关问题

设置了 max-age 后,为什么浏览器仍然请求源站?

可能是资源没有命中该 handler、请求带有认证信息、代理覆盖了响应头,或者客户端正在执行重新加载策略。先区分响应是否来自浏览器缓存、CDN 还是源站,再看最终响应头。

HTML 也能用一年缓存吗?

可以,但必须有可靠的版本化入口或发布失效机制。没有这些配套时,HTML 使用长缓存会让用户继续引用旧的 JS、CSS 资源,通常不如 no-cache 稳妥。

http.FileServer 会自动设置 Cache-Control 吗?

不要把它当作应用缓存策略配置器。它负责静态文件响应流程,缓存时长应由外层 handler、反向代理或 CDN 规则明确设置。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>