客户端缓存必须在具体路由handler中显式设置Cache-Control和ETag,因全局中间件无法获取业务数据上下文、无法适配不同接口的过期策略与参数差异,易导致缓存污染或失效。

不能靠中间件统一设,必须在具体路由 handler 里用 c.Response().Header().Set() 显式写入 Cache-Control 和 ETag 等头;否则浏览器根本不会缓存。
为什么全局中间件不适合设客户端缓存
客户端缓存(即浏览器/CDN 缓存)依赖响应头中的 Cache-Control、Expires、ETag、Last-Modified 等字段,而这些字段必须和具体业务逻辑强绑定:不同接口的过期时间、是否可被共享、是否需校验等策略完全不同。用中间件统一注入会导致:
-
Cache-Control: public, max-age=300被硬套在所有接口上,包括登录态相关的/user/profile - 动态生成的
ETag(如基于数据版本号或内容哈希)无法在中间件里获取业务数据上下文 - 忽略请求参数差异(如
?lang=zh和?lang=en应视为不同缓存项),导致缓存污染
在 handler 中设置 Cache-Control 的正确姿势
直接操作响应头,且优先使用语义明确的值。不要依赖框架默认行为。
开箱即用的技能链路由引擎。13 条预定义链覆盖搜索、开发、审查、MLOps、法律、创意等场景,三层路由架构(触发词→SAD反馈→DAG编排),recall@10=96.97%。配置驱动(chains.yaml),零代码扩展。pip install skill-weave-chains 一键安装。
- 静态资源或强不变接口(如版本化 API 文档):
c.Response().Header().Set("Cache-Control", "public, immutable, max-age=31536000") - 半动态列表接口(如分页用户数据,5 分钟内可接受旧数据):
c.Response().Header().Set("Cache-Control", "private, max-age=300") - 需校验但不常变的接口(如配置中心):
c.Response().Header().Set("Cache-Control", "no-cache"),再配合ETag或Last-Modified - 绝不能写
Cache-Control: no-store在高频读接口上——它禁用所有缓存,包括内存缓存,白白浪费带宽
如何安全生成 ETag 并支持 304 响应
ETag 不是随便拼字符串,必须能唯一标识响应体内容,且避免计算开销过大。
- 对 JSON 响应,推荐用数据结构的稳定哈希(如
sha256.Sum256)+ 版本字段(如 DB 记录的updated_at时间戳)拼接后 base64 编码 - 在 handler 开头检查
c.Request().Header.Get("If-None-Match"),若匹配则直接return c.NoContent(http.StatusNotModified) - 不要在
ETag中包含敏感信息(如用户 ID、token),否则可能泄露 - 避免用整个响应体做哈希——大 payload 下性能差;改用关键字段组合哈希(如
id, updated_at, version)
容易被忽略的兼容性细节
即使写了 Cache-Control,浏览器也可能不缓存,原因常藏在细节里:
- HTTP 状态码不是
200、203、204、206、300、301、410时,多数浏览器直接忽略缓存头 - 响应体为空(
c.NoContent())时,ETag和Last-Modified仍需设置,否则 304 校验失效 - 如果用了
c.JSON(),它内部会设Content-Type: application/json,但不会自动加charset=utf-8;缺这个会导致某些旧浏览器拒绝缓存 - CDN(如 Cloudflare)默认只缓存
GET/HEAD且状态码为200的响应;若你返回201 Created,哪怕有Cache-Control也会被跳过
真正生效的客户端缓存,永远是「每条路由自己决定」,而不是靠中间件一刀切。最麻烦的地方在于:ETag 计算和 304 判断必须和业务数据更新节奏严格对齐,差一秒都可能导致脏缓存或重复渲染。


















