直接用 if/else 堆逻辑在业务规则超5条且频繁变更时会失控,因规则与代码耦合导致测试难、上线风险高;应抽离规则至外部dsl,如go生态中轻量稳定的grule引擎,支持热重载、文本规则、变量绑定,但需注意大小写敏感、salience顺序、并发安全及热更新正确流程。

为什么直接用 if/else 堆逻辑很快会失控
当业务规则超过 5 条、且需要频繁变更(比如风控策略、促销配置、审批流),硬编码的 if/else 或 switch 会迅速变成“改一行,测三天”的状态。规则和代码耦合,测试难覆盖,上线前不敢动,运维看不懂——这不是写得不够好,是结构本身不支持动态演进。
真正需要的不是“更漂亮的条件分支”,而是把规则从代码里抽出来,让非开发人员也能看懂、修改、灰度发布。
github.com/hyperjumptech/grule-rule-engine 是目前最轻量可用的选择
Go 生态中成熟的规则引擎极少,grule 是少数能稳定用于生产、文档较全、不依赖反射黑魔法的方案。它用 DSL(类似 Java 的 Drools)描述规则,运行时编译为 Go 函数,性能接近原生代码,且支持热重载。
- 规则文件是纯文本(
.grl),可存 Git、配 Consul、甚至走 HTTP 接口拉取 - 变量绑定走
map[string]interface{}或 struct,无需预定义 schema - 不支持复杂函数链式调用(比如
user.GetOrders().Filter(...)),但支持基础方法调用和字段访问 - 注意:它不处理规则冲突或优先级调度,需靠
salience字段手动控制执行顺序
一个真实可用的促销规则示例(含易错点)
假设要实现“新用户首单满 100 减 20,老用户满 200 减 15,VIP 用户不参与”:
rule DiscountForNewUser "Apply discount for new users" salience 100 {
when
$order : Order($order.Total >= 100)
$user : User($user.IsNew == true && $user.Role != "vip")
then
$order.Discount = 20;
Log("Applied new-user discount");
}
rule DiscountForRegularUser "Apply discount for regular users" salience 50 {
when
$order : Order($order.Total >= 200)
$user : User($user.IsNew == false && $user.Role != "vip")
then
$order.Discount = 15;
}
关键细节:
-
$order和$user是绑定变量名,必须和传入的 struct 字段名大小写完全一致(Go 是大小写敏感的) -
salience值越大越先执行,但不会跳过后续规则——如果用户既是新用户又是 VIP,两条规则都可能触发,需靠条件里的&& $user.Role != "vip"排除 -
Log()是内置函数,但输出默认到os.Stdout,线上需替换为自定义 logger(通过ast.NewKnowledgeBaseInstance().AddLogger(...)注入) - 规则里不能直接调用外部包函数(如
time.Now()),必须提前注入为全局函数或绑定到对象上
热更新规则时最常踩的坑
规则热更新不是“替换文件就生效”,而是要重新编译知识库并替换运行时实例。常见失败场景:
- 忘记调用
kbase.LoadRuleFromBytes()后再调用kbase.BuildRuleGraph()—— 规则只是加载了,没编译成可执行图 - 多个 goroutine 并发调用
engine.Execute()时,复用同一个KnowledgeBaseInstance实例 —— 它不是并发安全的,每次执行应新建实例 - 规则语法错误(比如少括号、字段名拼错)只在
BuildRuleGraph()阶段报错,但错误信息极简(如"error on line 12"),建议配合 VS Code 的grule插件做编辑时校验 - 更新后没清空旧的
KnowledgeBaseInstance引用,GC 不及时导致内存缓慢增长 —— 建议用 sync.Pool 缓存实例,或显式置为 nil
规则引擎不是银弹。它解决的是“规则变太快、人太多、不敢动代码”的问题,而不是替代清晰的领域建模。一旦规则间开始出现强依赖(比如 A 规则输出是 B 规则输入),说明该拆服务了,别硬塞进一个引擎里。











