Go标准库不自动处理接口缓存,http.ServeFile和FileServer不适用于JSON API等动态响应;ETag需手动计算、校验,返回304时禁止携带响应体,否则客户端行为不可控。

Go 标准库不自动处理接口缓存,http.ServeFile 和 http.FileServer 都不适用于 JSON API 等动态响应;ETag 必须手动计算、设置、校验,且返回 304 Not Modified 时不能带响应体——否则客户端行为不可控。
为什么不能直接用 http.ServeContent 或 http.FileServer 做接口缓存
http.ServeContent 只接受 io.ReadSeeker,适合文件流,不适合 JSON、HTML 模板等需运行时生成的内容;http.FileServer 的 ETag 是基于文件系统元信息(如 ModTime + Size)算的,对 API 响应完全无效。它也不会读请求头里的 If-None-Match 并提前退出。
- 你返回的是
json.Marshal(v)结果,不是磁盘上某个固定文件 → 文件系统 ETag 机制失效 -
http.ServeContent要求传入modtime,但 API 响应没有“修改时间”概念,硬塞一个会导致语义错乱 - 即便你包装了
http.FileServer,它内部仍只认os.FileInfo,无法注入业务逻辑生成的 ETag
如何为 JSON API 手动实现 ETag 条件请求
核心是三件事:生成内容一致的 ETag、提前检查 If-None-Match、返回 304 时不写 body。别在 handler 末尾才判断,得在序列化前就决定是否退出。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
- ETag 值必须基于实际响应字节,推荐用
sha256.Sum256(bodyBytes),包裹双引号:"%x",不是%x - 用
r.Header.Get("If-None-Match")取值,注意它可能含多个 ETag(逗号分隔)或带W/"前缀;Go 1.19+ 可用http.EtagMatch()标准化比对 - 一旦决定返回
304,**绝不能调用json.NewEncoder(w).Encode()或w.Write()** —— 否则响应体已部分写出,WriteHeader(304)会 panic 或被忽略 - 示例关键片段:
body, _ := json.Marshal(data) etag := fmt.Sprintf(`"%x"`, sha256.Sum256(body)) if match := r.Header.Get("If-None-Match"); match != "" && http.EtagMatch(match, etag) { w.WriteHeader(http.StatusNotModified) return } w.Header().Set("ETag", etag) w.Header().Set("Cache-Control", "no-cache") w.Write(body)
ETag 计算时机和性能陷阱
每次请求都重新 json.Marshal + sha256.Sum256?小数据还行,大结构或高频接口会吃 CPU。真正要优化的不是哈希算法,而是避免重复计算。
立即学习“go语言免费学习笔记(深入)”;
- 如果响应数据来自 DB 查询且不变,可在查询后立刻算一次 ETag 并缓存到 struct 字段里,handler 直接取用
- 不要用
time.Now().Unix()、rand.Intn()、数据库自增 ID 当 ETag —— 它们和内容无关,缓存必然失效 - 模板渲染类响应(含
{{.Now}}或用户 ID)不适合强 ETag;可改用弱 ETag(W/"size-mtime"),或干脆禁用缓存(Cache-Control: no-store) - 若用内存缓存(如
map[string][]byte),记得加锁或用sync.Map,ETag 值必须和缓存体严格对应
最易被忽略的一点:返回 304 时,Content-Type、Content-Length 这类头必须清掉——RFC 明确规定 304 响应不能含这些字段。Go 的 http.ResponseWriter 不会自动清理,得靠你手动控制写入顺序和时机。

















