不能裸用 http.fileserver,因其不设 cache-control、不生成 etag、不处理 if-none-match,导致浏览器无法有效缓存;需手动包装并注入响应头、支持 range、动态计算 etag、显式清理缓存以保障一致性。

直接用 http.FileServer 不行——它默认不设 Cache-Control、不生成 ETag、也不处理 If-None-Match,浏览器每次都会重拉,缓存形同虚设。
为什么不能裸用 http.FileServer?
它只做最基础的文件读取和 MIME 推断,连 Cache-Control 都不写。实测响应头里空空如也,Chrome DevTools Network 面板会显示「(disk cache)」或「(memory cache)」完全不生效,所有请求都打到服务端。
-
http.ServeFile同样不设头,且无法自动 fallback 到index.html - 它内部用
os.Stat做Last-Modified,但不暴露给上层控制,也没法配max-age - 路径遍历防护只在
http.Dir是绝对路径时才可靠,相对路径(如"./static")在不同工作目录下可能失效
必须包装 http.FileServer 并手动注入响应头
核心是用 http.HandlerFunc 拦截,在 WriteHeader 前写头。顺序错了会 panic;晚于 ServeHTTP 调用就彻底无效。
- 对
.js、.css、.png等资源,设w.Header().Set("Cache-Control", "public, max-age=31536000, immutable") - 对
index.html或入口 HTML,必须设"no-cache"或"must-revalidate",否则更新后用户卡旧版 - 禁用
ETag仅当文件名带哈希(如app.a1b2c3.js)且内容不变——此时immutable+max-age已足够,多加ETag反而增加校验开销 - 不要在中间件里调
next.ServeHTTP后再写头,那已经太晚
大文件(>10MB)跳过内存缓存,只靠 HTTP 协议层减载
把整份日志或视频读进 sync.Map,GC 压力陡增,还可能触发 OOM。这类文件应绕过内存层,专注用好 Cache-Control 和 Range 支持。
- 用自定义
http.FileSystem包裹原路径,在Open()返回前写Cache-Control: public, max-age=3600 -
ETag必须动态算:若文件可能被logrotate修改,就不能硬编码哈希,得用os.Stat().ModTime().UnixNano()或md5.Sum(fileBytes)(小文件才适合后者) - 务必支持
Range请求——http.ServeContent自动处理,http.FileServer不支持,需手动回退 - 外部进程改了文件,Go 进程不会自动感知;所有写操作(如上传接口)完成后,必须显式调
fileCache.Delete(filename)
小文件走内存缓存要防泄漏和扫描慢
sync.Map 看似简单,但没过期机制,Range 扫描上万条目时 CPU 明显升高,且已 Delete 的 key 仍占内存。
- 数量超几百条就别用
sync.Map;改用github.com/bluele/gcache,注意初始化必须调Build(),否则Get()直接 panic -
ExpireIn(-1 * time.Second)不报错,但会被当永不过期——负数陷阱容易漏查 - 避免用
filepath.Join(root, r.URL.Path)拼路径再传给FileServer;先filepath.Clean,再检查是否仍在白名单根目录内 - 静态资源建议构建时嵌入二进制(
embed.FS),但注意它默认不启用 gzip,也不自动设Content-Encoding
真正麻烦的不是设头,而是缓存一致性:文件被外部修改时,你既不能靠 inotify(容器里常不可用),也不能靠全量 os.Stat 扫描(I/O 开销大)。最稳的方案是——所有写入口都配套清缓存逻辑,别指望自动发现。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











