Beego中间件不能直接做页面缓存,需手动包装ResponseWriter并在Prepare()中替换、Finish()中捕获响应体,结合Cache-Control头与Redis/内存缓存实现;静态资源须用BeforeStatic拦截,动态页面缓存key须含Method、Path及Accept-Language等字段。

Beego 中间件不能直接做页面缓存,得靠 ResponseWriter 包装 + Cache-Control 头
Beego 没有内置的「页面级缓存中间件」,InsertFilter 注册的函数只能操作 *context.Context,无法拦截并缓存整个 HTTP 响应体。真要实现页面缓存(比如 /article/123 返回的 HTML),必须手动包装 ctx.ResponseWriter,在 WriteHeader 和 Write 时捕获输出,并结合 cache 模块存取。
- 不能依赖
beego.Cache的Put/Get直接缓存 HTML 字符串——它只适合结构化数据,不处理 ETag、Last-Modified、Vary 等 HTTP 缓存语义 - 静态资源(
/static/)必须用beego.BeforeStatic阶段拦截,但该阶段拿不到ctx.Output.Body,无法读响应内容 - 动态页面缓存唯一可行位置是
beego.BeforeExec或beego.AfterExec,但AfterExec已经写完响应,只能记录日志,不能改内容 - 真正能拦截并缓存响应的位置,是在 Controller 的
Prepare()里提前包装ctx.ResponseWriter,再在Finish()里判断是否命中并返回
手动包装 ResponseWriter 实现页面缓存的关键步骤
核心思路:在请求进入 Controller 前,把原始 http.ResponseWriter 替换为一个带缓冲的 wrapper;Controller 执行完后,检查是否需要缓存,再把缓冲内容写回原 ResponseWriter 并存入 Redis/memory。
- 缓存 key 必须包含
ctx.Request.Method + ctx.Request.URL.Path + ctx.Request.Header.Get("Accept-Language"),否则多语言页面会错乱 - 必须在
Prepare()中调用ctx.ResponseWriter = &bufferedResponseWriter{...},不能在Get()里——那时 Header 已部分写出 - 不能缓存含 Session 或用户私有数据的页面(如
/user/profile),需在生成 key 前用正则排除或加白名单路径 - 缓存过期时间建议设为 5–30 分钟,避免用
time.Now().Add(24 * time.Hour)这种长 TTL 导致内容 stale - 示例片段:
type bufferedResponseWriter struct { http.ResponseWriter buf *bytes.Buffer } func (w *bufferedResponseWriter) Write(b []byte) (int, error) { return w.buf.Write(b) } // 在 Prepare() 中: ctx.ResponseWriter = &bufferedResponseWriter{ResponseWriter: ctx.ResponseWriter, buf: &bytes.Buffer{}} // 在 Finish() 中: if shouldCache(ctx) { cache.Put(cacheKey, w.buf.Bytes(), 600*time.Second) } w.ResponseWriter.Write(w.buf.Bytes())
为什么别用 beego.BeforeRouter 做页面缓存
beego.BeforeRouter 阶段确实最早,但此时 ctx.Input.Param() 不可用,URL 参数未解析,你根本没法构造稳定缓存 key。更严重的是:静态资源(/static/xxx)完全不走这个阶段,导致你写的缓存逻辑对 CSS/JS 文件失效,而这些恰恰最该被 CDN 或浏览器缓存。
- 常见错误:在
BeforeRouter里调用ctx.Input.Param(":id"),结果返回"",key 变成GET|/article/|zh-CN,所有文章都命中同一个缓存 - 正确做法是把缓存逻辑下沉到 Controller 层,在
Prepare()中用ctx.Input.URI()或ctx.Request.URL.String()构造 key,确保参数已归一化 - 如果硬要在过滤器里做,必须用
beego.BeforeExec,此时ctx.Input.Param()已就绪,且还没进 Controller,还能 abort
Redis 缓存页面内容时容易忽略的配置点
用 Beego 的 Redis cache 驱动存 HTML 页面,不是配对 host/password 就完事。页面内容体积大(常超 10KB),默认连接池和序列化方式会出问题。
-
PoolSize至少设为 20,否则高并发下 Redis 连接等待超时,缓存写入失败但无报错 - 必须显式设置
"conn_max_lifetime": 3600,否则空闲连接被 Redis server 断开后,Beego 不重连,后续请求全走 DB - HTML 字符串不要用
cache.Put(key, htmlStr, ttl)直接存——Beego 的 Redis driver 默认用gob序列化,会把字符串转成二进制乱码;应改用cache.Put(key, []byte(htmlStr), ttl) - 用
cache.IsExist(key)判断前,先确认 Redis 中该 key 是否真的存在(用redis-cli get key),因为 Beego 的IsExist在网络异常时可能返回false误判
页面缓存最难的不是存和取,而是失效策略。Beego 没提供缓存标签(cache tag)或通配清除能力,删一个文章的缓存,得手动算出所有相关 URL(列表页、详情页、API 接口)再逐个 Delete,漏一条就产生脏数据。


















