ServeContent 并非不处理 If-Modified-Since 和 If-None-Match,而是依赖传入的 modtime 和 etag 参数自动处理:若 modtime 非零且匹配 If-Modified-Since,或 etag 非空且严格匹配 If-None-Match,即返回 304;否则返回 200。

为什么 ServeContent 不自动处理 If-Modified-Since 和 If-None-Match
ServeContent 本身不解析或响应条件请求头,它只负责按需写入内容、设置状态码和基础头(如 Content-Length)。是否返回 304 Not Modified 完全取决于你传给它的 modtime 和 etag 参数,以及客户端是否发送了匹配的条件头。
常见错误是直接传入一个固定时间(比如 time.Now())或空 etag,导致每次请求都走 200 路径;或者忽略了 http.ServeContent 对 modtime 的要求——它必须是文件/资源的真实最后修改时间,且不能为零值(time.Time{})。
实操建议:
- 确保
modtime是资源真实、稳定的最后修改时间(例如从文件os.FileInfo.ModTime()或数据库字段读取),不是每次请求生成的时间 -
etag应为非空字符串,推荐用强 ETag(如"W/" + base64.StdEncoding.EncodeToString(hash[:])不推荐;直接用fmt.Sprintf("%x", hash)更稳妥) - 如果资源无明确修改时间,可设
modtime为time.Time{},此时ServeContent会忽略If-Modified-Since,但仍检查If-None-Match(前提是etag非空)
如何构造符合 RFC 7232 的 etag 并传给 ServeContent
ServeContent 不校验 etag 格式,但客户端是否正确缓存取决于你是否遵循规范。弱 ETag(以 W/ 开头)仅用于语义等价比较,强 ETag 才支持字节级验证 —— 大多数静态资源应使用强 ETag。
示例中常见错误是把整个文件内容直接当 ETag(大文件内存爆炸),或用 md5.Sum([]byte(filename)) 这类不反映内容变更的哈希。
实操建议:
- 小文件:用
md5.Sum(fileBytes),但注意提前读取并限制大小(如 ≤1MB) - 大文件或流式场景:基于文件元信息(size + modtime)生成 ETag,例如
fmt.Sprintf("%d:%d", fi.Size(), fi.ModTime().UnixNano())—— 简单、稳定、免 IO - 务必去掉引号:传给
ServeContent的etag是纯字符串(如"123456:7890"),不是"\"123456:7890\"";否则客户端无法匹配
完整 handler 示例:带条件请求支持的文件服务
下面是一个最小可行的 http.HandlerFunc,演示如何安全调用 http.ServeContent 并支持标准条件请求:
func serveStaticFile(w http.ResponseWriter, r *http.Request) {
// 假设已通过路径查到本地文件
f, err := os.Open("/path/to/file.js")
if err != nil {
http.Error(w, "Not Found", http.StatusNotFound)
return
}
defer f.Close()
fi, _ := f.Stat()
if !fi.Mode().IsRegular() {
http.Error(w, "Not a file", http.StatusForbidden)
return
}
// 构造 ETag:强 ETag,基于 size + modtime
etag := fmt.Sprintf("%d:%d", fi.Size(), fi.ModTime().UnixNano())
// ServeContent 自动判断是否返回 304
http.ServeContent(w, r, fi.Name(), fi.ModTime(), &fileReader{f})
}
type fileReader struct{ *os.File }
func (r *fileReader) Read(p []byte) (n int, err error) { return r.File.Read(p) }
func (r *fileReader) Seek(offset int64, whence int) (int64, error) { return r.File.Seek(offset, whence) }
关键点:
-
http.ServeContent要求io.ReadSeeker,所以包装了*os.File;不能直接传*bytes.Reader(不支持Seek) - 没手动设置
Content-Type:由ServeContent内部根据文件名后缀推断(依赖net/http.DetectContentType的启发式逻辑) - 没显式写
ETag或Last-Modified头:这些由ServeContent自动添加,前提是etag非空、modtime有效
调试条件请求失败的三个检查点
当你发现客户端始终收不到 304,先盯住这三处:
- 用
curl -I -H "If-Modified-Since: $(date -R)" http://localhost/file.js手动测试,确认服务端是否返回304;不要只信浏览器开发者工具里的“from cache”字样 - 检查响应头里是否有
ETag和Last-Modified:若缺失,说明etag为空或modtime.IsZero()为 true - 对比请求头
If-None-Match值与响应头ETag是否完全一致(包括引号):很多前端构建工具(如 Webpack)会在 ETag 外加双引号,而ServeContent不加 —— 此时需在生成 ETag 时手动包裹:etag = `"` + etag + `"`
ETag 匹配是严格字符串相等,没有自动去引号逻辑。这个细节漏掉,条件请求就彻底失效。


















