Revel panic 捕获需手动注册拦截器、包装子 goroutine、捕获 init 阶段 panic 并确保日志输出可见;缺一不可,否则 panic 静默丢失。

Revel 的 panic 处理不是靠“配置文件开关”启用的,而是必须手动注册拦截器 + 补全 recover 逻辑,否则子 goroutine panic 一定静默丢弃。
revel.InterceptFunc(recoverPanic, revel.PANIC) 必须显式注册
Revel 默认的 panic 拦截器只在 app/interceptors.go 中注册一次,但容易被注释、条件跳过或误删。不检查这行代码,等于没开 panic 捕获。
- 打开
app/interceptors.go,确认存在且未被注释:revel.InterceptFunc(recoverPanic, revel.PANIC) - 该行必须在
init()函数中执行,不能放在 if 分支里(比如if revel.RunMode == "dev") - 如果用了自定义
recoverPanic函数,确保它内部调用debug.Stack(),而不是只打印r—— 否则日志里只有runtime error: invalid memory address,没有堆栈行号
裸 go 语句是 panic 黑洞,必须用 safeGo 包装
HTTP handler 里写 go sendEmail() 或 time.AfterFunc(5*time.Second, fn),里面的 panic 永远不会进 recoverPanic,因为不在主请求 goroutine。
- 全局搜索项目中所有
go(注意空格),逐行检查是否裸调用 - 第三方 SDK 的回调也属于子 goroutine:数据库
db.QueryRow().Scan()后的 defer、消息队列consumer.Ack()回调、HTTP client 的http.Do()后续处理,全部要包一层 - safeGo 定义必须带
debug.Stack():func safeGo(fn func()) { go func() { defer func() { if r := recover(); r != nil { log.Panicf("goroutine panic: %v\n%v", r, debug.Stack()) } }() fn() }() }
init() 阶段和 main goroutine panic 要单独捕获
Revel 启动失败、init() 里 panic、或者 revel run 过程中 panic,都不会走 revel.PANIC 拦截器链 —— 因为还没进 HTTP 生命周期。
- 在
app/init.go的init()函数最开头加:defer func() { if r := recover(); r != nil { log.Panicf("main panic: %v\n%v", r, debug.Stack()) } }() - 这个 defer 只捕获当前 goroutine,所以它能兜住 init 阶段、main 函数、以及 Revel 启动流程本身的 panic
- 不要省略
debug.Stack()—— 没它就只能看到 panic 类型,找不到第几行
panic 日志里看不到具体文件行号?检查 log 输出目标
即使 recover 成功,日志也可能输出到黑洞:Revel 默认把 log.Panicf 写进 revel.INFO.log,但开发时很多人只盯终端 stdout。
- 确认
conf/app.conf中log.output.path指向可写路径,比如log.output.path = logs/revel.log - 终端看不到 panic 日志?别只刷新浏览器,直接
tail -f logs/revel.log实时看 - 如果用 Docker,确保 logs/ 目录挂载出来,否则容器重启后日志全丢
真正卡住排查的从来不是 panic 发生在哪,而是它根本没被记录——子 goroutine、init 阶段、日志路径错位,这三个点漏掉一个,问题就变成玄学。











