recover必须在规则函数内部显式包裹,因panic会导致goroutine终止且顶层defer无效;每个规则单元需自带defer recover保护并记录上下文日志,不可替代error处理,且须防范goroutine泄漏。

recover 必须在规则函数内部显式包裹
规则引擎里执行用户自定义逻辑(比如 evalRule、Run 或 Execute 方法)时,一旦规则体里出现 panic(空指针、除零、map 写 nil、类型断言失败),整个 goroutine 会直接终止——而这不是你希望的。HTTP handler 或 main 的顶层 defer recover() 对它无效,因为规则通常跑在独立 goroutine 或回调中。
正确做法是:每个可执行规则单元都自带一层保护:
defer func() { if r := recover(); r != nil { log.Warn("rule panic", "rule_id", id, "err", r) } }()- 不要只打印日志,建议附带规则上下文(如输入数据 hash、触发条件字段)
- 避免在 recover 后继续执行后续规则逻辑,除非你明确设计了“跳过当前规则继续下一条”的语义
recover 不能替代 error 显式处理
规则引擎里大量依赖外部调用:DB 查询、HTTP API、Redis 获取配置、正则匹配等。这些操作本该返回 error,但有人图省事写成 mustGetConfig() 或 MustCompileRegex()——它们内部 panic,再靠 recover 拦住。这等于把可预判、可分类、可重试的错误,降级为不可控的运行时崩溃。
真正稳健的做法是:
立即学习“go语言免费学习笔记(深入)”;
- 所有规则函数签名应返回
(result interface{}, err error),而非仅interface{} - 对
regexp.Compile、json.Unmarshal、db.QueryRow.Scan等必须检查err != nil,并映射为业务错误码(如ErrRuleInvalidInput) - recover 只兜底那些你根本没预料到的 panic(比如第三方库 bug、unsafe 操作越界),不是 error 的替代品
goroutine 泄漏比 panic 更危险
规则引擎常配合定时任务(如每分钟评估风控策略)或事件驱动(如 Kafka 消息触发规则流)。若你在规则里启了子 goroutine 却没管生命周期,比如:
go func() {
result := callExternalAPI(input) // 没带 context.WithTimeout
ch <- result
}()
一旦 callExternalAPI 卡死,这个 goroutine 就永远悬停,且无法被上层 cancel。连续跑几小时,可能积压数百个,吃光内存。
必须做到:
- 所有异步调用必须传入带超时的
context.Context,超时后主动关闭 channel 或标记失败 - 用
sync.WaitGroup等待时,wg.Add(1)必须在go前,且子 goroutine 内部defer wg.Done() - 规则执行超时应由引擎统一控制(如
ctx, cancel := context.WithTimeout(parentCtx, 500*time.Millisecond)),而不是靠 recover 猜它挂了没
panic 类型决定是否该 recover
不是所有 panic 都适合被 recover。Go 标准库明确说明:runtime.Goexit、os.Exit、以及向已关闭 channel 发送值引发的 panic,都不能被 recover 捕获;而像 nil pointer dereference、index out of range 这类可以。
更关键的是语义判断:
- 如果 panic 来自用户规则脚本(比如 Lua/JS 解释器内抛出异常),应该 recover 并转为
RuleExecutionError上报,不影响引擎主流程 - 如果 panic 来自引擎核心(比如规则 AST 编译器内部空指针),说明状态已损坏,recover 后不应继续服务,而应记录 fatal 日志并触发进程级健康检查
- 对
reflect.Value.Call引发的 panic,需用errors.Is(r, reflect.ValueOf(nil).Call(nil))做类型判断,避免误吞关键错误
recover 是手术刀,不是创可贴;用错位置,反而掩盖真正该修复的规则边界校验缺失或资源初始化缺陷。


















