ETag 必须带双引号(如 "abc123" 或 W/"abc123"),Gin 需手动计算并设置;校验时需严格比对含引号的 If-None-Match,匹配成功须用 c.Status(304).End() 返回无体响应,并配合 Cache-Control: max-age=0 触发协商。

ETag 生成必须带引号且区分强弱校验
不加引号的 ETag 值(如 abc123)会被浏览器视为非法,直接忽略协商逻辑;必须包裹双引号,如 "abc123"(强校验)或 W/"abc123"(弱校验)。Gin 默认不生成 ETag,需手动计算并写入响应头。
强校验要求字节级一致,适合静态内容或内容哈希稳定的 API;弱校验允许语义等价(比如 HTML 中空格/换行差异不影响),但 Gin 本身不支持自动生成弱 ETag,需上游或业务层注入 W/"xxx" 字符串。
- 使用
c.Header("ETag", `"${hash}"`)设置强 ETag(注意外层双引号 + 内层 hash) - 若用弱校验,必须显式拼接
W/"${hash}",Nginx 或反向代理层不会帮你补前缀 - 避免用时间戳单独作 ETag(如
"20260811"),它不具备内容唯一性,易导致误 304
Gin 中校验 If-None-Match 并返回 304 的正确姿势
关键不是“有没有 ETag”,而是请求进来时是否检查了 If-None-Match 头,并在匹配时 **立即终止响应体发送**。Gin 的 c.Status(304).Data(...) 或 c.JSON(304, ...) 都会出错——304 不允许带响应体。
正确做法是:比对成功后调用 c.Status(304).Header("ETag", etag).End()(End() 是 Gin v1.9+ 提供的无体响应方法),或更兼容地用 c.Writer.WriteHeader(304) + c.Writer.Flush()。
- 必须先读取
c.Request.Header.Get("If-None-Match"),注意它是原始字符串(含引号) - 比对时要严格相等,包括引号 —— 即
ifNoneMatch == etag,而非去掉引号再比 - 一旦返回 304,后续任何
c.JSON/c.String都会 panic,务必提前 return
Cache-Control 与 ETag 必须协同生效
只设 ETag 不设缓存策略,浏览器可能直接跳过协商流程;只设 Cache-Control: no-cache 而没 ETag,就会退化为 Last-Modified 校验或直接 200。两者是配合关系,不是二选一。
典型组合:Cache-Control: public, max-age=0, must-revalidate 表示“可缓存但每次都要验证”,此时浏览器必定携带 If-None-Match(如果有 ETag)或 If-Modified-Since(如果有 Last-Modified)。
-
max-age=0是触发协商缓存的最小安全值,比no-cache更明确表达“需验证”意图 - 不要用
no-store—— 它禁用所有缓存,ETag 完全失效 - 动态接口慎用
immutable,它会阻止后续协商,哪怕 ETag 已变
常见踩坑:开发环境调试时协商缓存被静默绕过
Chrome DevTools 勾选了 Disable cache,或刷新页面时按了 Ctrl+R(而非 Enter),会导致请求头自动带上 Cache-Control: max-age=0 + Pragma: no-cache,此时即使服务端返回了正确 ETag,浏览器也跳过 304 流程,直接走 200。
另一个隐蔽问题:Gin 中间件顺序。如果 Logger 或自定义中间件在 ETag 生成前修改了响应体(比如注入 trace ID),ETag 就和实际响应体不一致,导致永远不匹配。
- 验证是否真走了协商:看 Network 面板中响应状态码是
304还是200,且响应体大小应为 0 - 用 curl 手动测试最可靠:
curl -I -H 'If-None-Match: "xxx"' http://localhost:8080/api/data - ETag 计算必须基于最终写出的响应体字节,不能基于结构体或中间变量


















