应使用拦截器封装公共逻辑,因其能自动注入请求链、共享控制器实例状态;避免基类继承、全局变量或模块滥用,工具函数需显式传入*revel.Controller。

用嵌入式拦截器(Interceptor)封装公共逻辑
Revel 不支持传统意义上的“基类控制器继承”,强行写一个 BaseController 并让其他 controller 嵌入它,会导致字段和方法在每次请求中重复初始化、无法统一管理生命周期,还可能破坏 *revel.Controller 的嵌入约定。真正被框架认可的方式是拦截器:它能自动注入到指定控制器的请求链中,在 Action 执行前后运行,且天然共享控制器实例状态。
常见错误现象:panic: interface conversion: interface {} is *app.BaseController, not *revel.Controller —— 这通常是因为你在拦截器里错误地尝试类型断言了一个非标准嵌入结构。
- 拦截器函数必须接收
*revel.Controller指针作为第一个参数,且只能通过c.Interceptors.Add(...)或全局revel.InterceptFunc注册 - 若只对某几个控制器生效,应在控制器 struct 定义时显式调用
c.Interceptors.Add(authCheck, revel.BEFORE) - 若需跨控制器复用(比如权限检查、用户加载),推荐在
app/init.go中用revel.InterceptFunc绑定,并配合revel.BEFORE或revel.FINALLY阶段
把可复用逻辑抽成独立函数 + 传入 *revel.Controller
不是所有逻辑都需要走拦截器。比如格式化时间、拼接 URL、生成分页链接这类无副作用的工具行为,直接写成普通函数更轻量、更易测试。关键点在于:这些函数如果需要访问会话、请求参数或 Flash 消息,就必须显式传入 *revel.Controller,而不是试图从某个全局上下文里“取”出来。
性能影响很小,但容易踩的坑是:有人会把 *revel.Controller 存进包级变量,导致并发请求间数据污染——Revel 每次请求都会新建 controller 实例,绝不能跨请求复用指针。
- 正确示例:
func FormatTime(c *revel.Controller, t time.Time) string { return c.RenderArgs["locale"].(*i18n.Locale).FormatTime(t) } - 错误做法:在函数内部调用
revel.CurrentController(该函数已废弃且线程不安全) - 路径相关逻辑(如重定向前保存来源页)建议用
c.Request.Referer()+c.Session.Set("return_url", ...)组合,而非依赖全局 state
避免用全局变量或 init 函数模拟“共享逻辑”
有些开发者会在 app/init.go 的 init() 函数里注册一堆全局函数或 map,再在控制器里手动调用。这种写法看似省事,实则绕过了 Revel 的依赖注入和生命周期管理机制,导致:
- 单元测试困难:无法 mock 全局函数行为
- 热重载失效:
init()只执行一次,代码修改后逻辑不会更新 - 并发不安全:多个 goroutine 同时读写同一 map 而无锁保护
如果你发现不得不靠全局变量来“共享”,大概率是职责没拆清——把数据获取、校验、转换等步骤混在同一个函数里,不如拆成小函数,按需传参调用。
模块(Module)适合跨项目复用,不适合控制器间共享
Revel 的 Module 是为了解决“多个应用共用一套用户中心/支付 SDK”这类场景设计的,它有自己的 controllers/、views/ 和路由,本质是插件化子系统。把它当成控制器间共享逻辑的手段,就像用 Docker Compose 启一个 Redis 容器来存 session 数据——过度设计,增加部署和调试复杂度。
真正该用模块的情况:你有一套完整的 OAuth2 登录流程(含 callback controller、token refresh logic、view 模板),要同时接入三个内部 Revel 应用。除此之外,别碰模块。
最常被忽略的一点:拦截器里的错误处理必须显式返回 revel.Result(比如 c.Redirect(...) 或 c.RenderError(...)),否则即使逻辑执行失败,请求也会继续往下走,造成静默异常。这点没有编译提示,全靠人工检查。


















