B站新一代 golang规则引擎gengine基础语法
作者:萧楚河
前言
gengine是一款基于golang和AST(抽象语法树)开发的规则引擎,gengine支持的语法是一种自定义的DSL
gengine于2020年7月由哔哩哔哩(bilibili.com)授权开源
gengine现已应用于B站风控系统、流量投放系统、AB测试、推荐平台系统等多个业务场景
你也可以将gengine应用于golang应用的任何需要规则或指标支持的业务场景
优势
| 对比 | drools | gengine |
|---|---|---|
| 执行模式 | 仅支持顺序模式 | 支持顺序模式、并发模式、混合模式,以及其他细分执行模式 |
| 规则编写难易程度 | 高,与java强相关 | 低,自定义简单语法,与golang弱相关 |
| 规则执行性能 | 低、无论是规则之间还是规则内部,都是顺序执行 | 高,无论是规则间、还是规则内,都支持并发执行.用户基于需要来选择合适的执行模式 |
开源代码地址
https://github.com/bilibili/gengine
https://github.com/bilibili/gengine
语法
DSL语法
const rule = ` rule "rulename" "rule-describtion" salience 10 begin //规则体 end`
如上,gengine DSL完整的语法块由如下几个组件构成:
关键字rule,之后紧跟"规则名称"和"规则描述",规则名称是必须的,但规则描述不是必须的. 当一个gengine实例中有多个规则时,"规则名"必须唯一,否则当有多个相同规则名的规则时,编译好之后只会存在一个
关键字salience,之后紧跟一个整数,表示的规则优先级,它们为非必须.数字越大,规则优先级越高;当用户没有显式的指明优先级时,规则的优先级未知, 如果多个规则的优先级相同,那么在执行的时候,相同优先级的规则执行顺序未知
关键字begin和end包裹的是规则体,也就是规则的具体逻辑
规则体语法
规则体的语法支持或执行顺序,与主流的计算计语言(如golang、java、C/C++等)一致
规则体支持的运算
支持完整数值之间的加(+)、减(-)、乘(*)、除(/)四则运算,以及字符串之间的加法
完整的逻辑运算(&&、 ||、 !)
支持比较运算符: 等于(==)、不等于(!=)、大于(>)、小于(<)、大于等于(>=)、小于等于(<=)
支持+=, -=, *=, /=
支持小括号
优先级:括号, 非, 乘除, 加减, 逻辑运算(&&,||) 依次降低
规则体支持的基础数据类型
string
bool
int, int8, int16, int32, int64
uint, uint8, uint16,uint32, uint64
float32, float64
不支持的特例
不支持直接处理nil,但用户可以在rule中定义一个变量去接受nil,然后再定义一个函数去处理nil
为了用户使用方便,最新版gengine已经内置了isNil()函数,用户可以直接使用,用于判断数据是否为nil
规则体支持的语法
完整的if .. else if .. else 语法结构,及其嵌套结构
其他语法大家如有需求可以去官方文档查看:
https://github.com/bilibili/gengine/wiki/%E8%AF%AD%E6%B3%95
使用案例
package test
import (
"bytes"
"fmt"
"github.com/bilibili/gengine/builder"
"github.com/bilibili/gengine/context"
"github.com/bilibili/gengine/engine"
"github.com/sirupsen/logrus"
"io/ioutil"
"strconv"
"strings"
"testing"
"time"
)
//定义想要注入的结构体
type User struct {
Name string
Age int64
Male bool
}
func (u *User)GetNum(i int64) int64 {
return i
}
func (u *User)Print(s string){
fmt.Println(s)
}
func (u *User)Say(){
fmt.Println("hello world")
}
//定义规则
const rule1 = `
rule "name test" "i can" salience 0
begin
if 7 == User.GetNum(7){
User.Age = User.GetNum(89767) + 10000000
User.Print("6666")
}else{
User.Name = "yyyy"
}
end
`
func Test_Multi(t *testing.T){
user := &User{
Name: "Calo",
Age: 0,
Male: true,
}
dataContext := context.NewDataContext()
//注入初始化的结构体
dataContext.Add("User", user)
//init rule engine
ruleBuilder := builder.NewRuleBuilder(dataContext)
start1 := time.Now().UnixNano()
//构建规则
err := ruleBuilder.BuildRuleFromString(rule1) //string(bs)
end1 := time.Now().UnixNano()
logrus.Infof("rules num:%d, load rules cost time:%d", len(ruleBuilder.Kc.RuleEntities), end1-start1 )
if err != nil{
logrus.Errorf("err:%s ", err)
}else{
eng := engine.NewGengine()
start := time.Now().UnixNano()
//执行规则
err := eng.Execute(ruleBuilder,true)
println(user.Age)
end := time.Now().UnixNano()
if err != nil{
logrus.Errorf("execute rule error: %v", err)
}
logrus.Infof("execute rule cost %d ns",end-start)
logrus.Infof("user.Age=%d,Name=%s,Male=%t", user.Age, user.Name, user.Male)
}
}示例解释
User是需要被注入到gengine中的结构体;结构体在注入之前需要被初始化;结构体需要以指针的形式被注入,否则无法在规则中改变其属性值
rule1是以字符串定义的具体规则
dataContext用于接受注入的数据(结构体、方法等)
ruleBuilder用于编译字符串形式的规则
engine接受ruleBuilder,并以用户选定的执行模式执行加载好的规则
小技巧
通过示例可以发现,规则的编译构建和执行是异步的. 因此,用户可使用此特性,在不停服的情况下进行更新规则.
需要注意的是,编译构建规则是一个CPU密集型的事情,通常只有规则被用户更新的时候才去编译构建更新;
gengine内部针对规则加载与移除已经做了很多的优化,gengine pool提供的所有相关的api也是线程安全且高性能的,因此我们建议您直接使用gengine pool
另外,用户还可以通过ruleBuilder来进行异步的语法检测.
如果大家对设计实现比较感兴趣可以通过如下地址查看:
//www.jb51.net/jiaoben/284935hzu.htm
以上就是B站新一代 golang规则引擎gengine基础语法的详细内容,更多关于B站go规则引擎gengine的资料请关注脚本之家其它相关文章!
