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的资料请关注脚本之家其它相关文章!