詳解如何使用Golang實現(xiàn)自定義規(guī)則引擎
規(guī)則引擎的功能可以簡化為當滿足一些條件時觸發(fā)一些操作,通常使用 DSL 自定義語法來表述。規(guī)則引擎需要先解析 DSL 語法形成語法樹,然后遍歷語法樹得到完整的語法表達式,最后執(zhí)行這些語法表達式完成規(guī)則的執(zhí)行。
本文以gengine來探討如何設計和實現(xiàn)一個自定義規(guī)則引擎。
支持的語句
為了滿足基本的業(yè)務規(guī)則需求,規(guī)則引擎應該要支持的語句有:
邏輯與算術運算
- 數(shù)學運算(+、-、*、/)
- 邏輯運算(&&、||、!)
- 比較運算(==、!=、>、<、>=、<=)
流程控制
- 條件(IF ELSE)
- 循環(huán) (FOR)
高級語句
- 對象屬性訪問(對象。屬性)
- 方法調用(func ())
規(guī)則語法的解析
規(guī)則的 DSL 語法定義應該簡單明了,gengine 使用了開源的語法解析器 Antlr4 來定義和解析規(guī)則語法。
定義規(guī)則語法
規(guī)則的 DSL 基本語法格式如下:
rule "rulename" "rule-describtion" salience 10 begin //規(guī)則體 end
其中規(guī)則體為具體規(guī)則語句,由上述的 邏輯與算術運算、流程控制、高級語句 組合而成。
例如,判斷為一個大額異常訂單的規(guī)則體:
if Order.Price>= 1000000 {
return
}
編寫解析器語法
Antlr4 解析器語法定義文件后綴名為.g4,以下內容為解析器的語法定義,解析器根據(jù)語法定義去逐行解析生成語法樹。
這里省略了一些非核心的語法定義并做了簡化,完整內容查看 gengine.g4。
grammar gengine;
primary: ruleEntity+;
// 規(guī)則定義
ruleEntity: RULE ruleName ruleDescription? salience? BEGIN ruleContent END;
ruleName : stringLiteral;
ruleDescription : stringLiteral;
salience : SALIENCE integer;
// 規(guī)則體
ruleContent : statements;
statements: statement* returnStmt?;
// 基本語句
statement : ifStmt | breakStmt;
expression : mathExpression
| expression comparisonOperator expression
| expression logicalOperator expression
;
mathExpression : mathExpression mathMdOperator mathExpression
| mathExpression mathPmOperator mathExpression
| expressionAtom
| LR_BRACKET mathExpression RR_BRACKET
;
expressionAtom
: functionCall
| constant
| variable
;
returnStmt : RETURN expression?;
ifStmt : IF expression LR_BRACE statements RR_BRACE elseIfStmt* elseStmt?;
elseStmt : ELSE LR_BRACE statements RR_BRACE;
constant
: booleanLiteral
| integer
| stringLiteral
;
functionArgs
: (constant | variable | functionCall | expression) (','(constant | variable | functionCall | expression))*
;
integer : MINUS? INT;
stringLiteral: DQUOTA_STRING;
booleanLiteral : TRUE | FALSE;
functionCall : SIMPLENAME LR_BRACKET functionArgs? RR_BRACKET;
variable : SIMPLENAME | DOTTEDNAME;
mathPmOperator : PLUS | MINUS;
mathMdOperator : MUL | DIV;
comparisonOperator : GT | LT | GTE | LTE | EQUALS | NOTEQUALS;
解析器生成語法樹
如,判斷為一個大額異常訂單的規(guī)則:
rule "order-large-price" "訂單大額金額" salience 10
begin
if Order.Price >= 1000000 {
return
}
end
語法解析器解析之后,生成語法樹:

遍歷語法樹生成語句表達式
解析器生成語法樹之后,只需要遍歷語法樹即可得到完整的語句表達式。Antlr4 解析器會生成 Listener 接口,這些接口在遍歷語法樹時會被調用。
type gengineListener interface {
antlr.ParseTreeListener
// 省略了一些只列舉了部分方法
// EnterRuleEntity is called when entering the ruleEntity production.
EnterRuleEntity(c *RuleEntityContext)
// ExitRuleEntity is called when exiting the ruleEntity production.
ExitRuleEntity(c *RuleEntityContext)
// EnterRuleContent is called when entering the ruleContent production.
EnterRuleContent(c *RuleContentContext)
// ExitRuleContent is called when exiting the ruleContent production.
ExitRuleContent(c *RuleContentContext)
// EnterStatement is called when entering the statement production.
EnterStatement(c *StatementContext)
// ExitStatement is called when exiting the statement production.
ExitStatement(c *StatementContext)
// EnterIfStmt is called when entering the ifStmt production.
EnterIfStmt(c *IfStmtContext)
// ExitIfStmt is called when exiting the ifStmt production.
ExitIfStmt(c *IfStmtContext)
// EnterExpression is called when entering the expression production.
EnterExpression(c *ExpressionContext)
// ExitExpression is called when exiting the expression production.
ExitExpression(c *ExpressionContext)
// EnterInteger is called when entering the integer production.
EnterInteger(c *IntegerContext)
// ExitInteger is called when exiting the integer production.
ExitInteger(c *IntegerContext)
}
可以發(fā)現(xiàn)在遍歷語法樹時,每個節(jié)點都有 EnterXXX() 和 ExitXXX() 方法存在,是成對出現(xiàn)的。
因此要遍歷語法樹只需要實現(xiàn) gengineListener 接口即可,gengine 巧妙的引入棧結構,遍歷完語法樹后(樹的遞歸遍歷就是進棧出棧過程),就得到了完整的規(guī)則語句表達式。這里只列舉部分方法,完整實現(xiàn)見 gengine_parser_listener。
type GengineParserListener struct {
parser.BasegengineListener
KnowledgeContext *base.KnowledgeContext
Stack *stack.Stack
}
func (g *GengineParserListener) EnterRuleEntity(ctx *parser.RuleEntityContext) {
if len(g.ParseErrors) > 0 {
return
}
entity := &base.RuleEntity{
Salience: 0,
}
g.ruleName = ""
g.ruleDescription = ""
g.salience = 0
g.Stack.Push(entity)
}
func (g *GengineParserListener) ExitRuleEntity(ctx *parser.RuleEntityContext) {
if len(g.ParseErrors) > 0 {
return
}
entity := g.Stack.Pop().(*base.RuleEntity)
g.KnowledgeContext.RuleEntities[entity.RuleName] = entity
}
gengine 通過解析器解析規(guī)則內容之后,規(guī)則的數(shù)據(jù)結構如下:

全局的 hashmap 以規(guī)則名為 key,規(guī)則體為 value,規(guī)則體中的 ruleContent 為該規(guī)則所有的語句表達式列表,列表中的值指向具體的語句表達式實體,語句表達式實體由 邏輯與算術運算、流程控制(IF、FOR)等基本語句組成。
規(guī)則語法的執(zhí)行
其實遍歷語法樹的過程中,將規(guī)則的執(zhí)行邏輯也放入 ExitXXX() 方法,這樣就能一并完成規(guī)則的解析和執(zhí)行。但是 gengine 沒有這么做,而是將規(guī)則的解析和執(zhí)行解耦,因為規(guī)則的解析往往只需要初始化一次,或者在規(guī)則有變更時熱更新解析,而規(guī)則的執(zhí)行則是在需要校驗規(guī)則的時候。
從 gengine 的規(guī)則數(shù)據(jù)結構可知,只需要遍歷全局的 hashmap,即可按順序執(zhí)行所有的規(guī)則(順序模式),執(zhí)行每一個規(guī)則后會通過addResult()方法記錄執(zhí)行結果:
// 順序模式
func (g *Gengine) Execute(rb *builder.RuleBuilder, b bool) error {
for _, r := range rb.Kc.RuleEntities {
v, err, bx := r.Execute(rb.Dc)
if bx {
// 記錄每個規(guī)則執(zhí)行結果
g.addResult(r.RuleName, v)
}
}
// 省略部分
...
}
對于某一個規(guī)則的執(zhí)行,則會去遍歷規(guī)則體 ruleContent 的所有語句表達式列表,然后按順序去執(zhí)行該規(guī)則下的所有語句表達式:
func (s *Statements) Evaluate(dc *context.DataContext, Vars map[string]reflect.Value) (reflect.Value, error, bool) {
for _, statement := range s.StatementList {
v, err, b := statement.Evaluate(dc, Vars)
if err != nil {
return reflect.ValueOf(nil), err, false
}
if b {
// return的情況不需要繼續(xù)執(zhí)行
return v, nil, b
}
if s.ReturnStatement != nil {
return s.ReturnStatement.Evaluate(dc, Vars)
}
return reflect.ValueOf(nil), nil, false
}
gengine 為每個語句類型都實現(xiàn)了 Evaluate() 方法,這里只討論 IF 語句的執(zhí)行:
type IfStmt struct {
Expression *Expression
StatementList *Statements
ElseIfStmtList []*ElseIfStmt
ElseStmt *ElseStmt
}
func (i *IfStmt) Evaluate(dc *context.DataContext, Vars map[string]reflect.Value) (reflect.Value, error, bool) {
// 執(zhí)行條件表達式
it, err := i.Expression.Evaluate(dc, Vars)
if err != nil {
return reflect.ValueOf(nil), err, false
}
// 執(zhí)行條件為真時的語句
if it.Bool() {
if i.StatementList == nil {
return reflect.ValueOf(nil), nil, false
} else {
return i.StatementList.Evaluate(dc, Vars)
}
}
return reflect.ValueOf(nil), nil, false
}
其中條件表達式Expression.Evaluate()為計算條件表達式的值:
func (e *Expression) Evaluate(dc *context.DataContext, Vars map[string]reflect.Value) (reflect.Value, error) {
// 原子表達式
var atom reflect.Value
if e.ExpressionAtom != nil {
evl, err := e.ExpressionAtom.Evaluate(dc, Vars)
if err != nil {
return reflect.ValueOf(nil), err
}
atom = evl
}
// 比較操作
if e.ComparisonOperator != "" {
// 計算左值
lv, err := e.ExpressionLeft.Evaluate(dc, Vars)
if err != nil {
return reflect.ValueOf(nil), err
}
// 計算右值
rv, err := e.ExpressionRight.Evaluate(dc, Vars)
if err != nil {
return reflect.ValueOf(nil), err
}
// 省略了類型轉化
switch e.ComparisonOperator {
case "==":
b = reflect.ValueOf(lv == rv)
break
case "!=":
b = reflect.ValueOf(lv != rv)
break
case ">":
b = reflect.ValueOf(lv > rv)
break
case "<":
b = reflect.ValueOf(lv < rv)
break
case ">=":
b = reflect.ValueOf(lv >= rv)
break
case "<=":
b = reflect.ValueOf(lv <= rv)
break
}
}
}
遞歸執(zhí)行到ExpressionAtom.Evaluate()原子表達式時,就可以得到該原子表達式的值以結束遞歸:
func (e *ExpressionAtom) Evaluate(dc *context.DataContext, Vars map[string]reflect.Value) (reflect.Value, error) {
if len(e.Variable) > 0 {
// 是變量則取變量值,通過反射獲取注入的自定義對象值
return dc.GetValue(Vars, e.Variable)
} else if e.Constant != nil {
// 是常量就返回值
return e.Constant.Evaluate(dc, Vars)
}
// 省略部分
}
支持自定義對象注入
在上下文中注入自定義對象后,就可以在規(guī)則中使用注入的對象。使用例子:
// 規(guī)則體
rule "test-object" "測試自定義對象" salience 10
begin
// 訪問自定義對象Order
if Order.Price >= 1000000 {
return
}
end
// 注入自定義對象Order
dataContext := gctx.NewDataContext()
dataContext.Add("Order", Order)
現(xiàn)在來看下 gengine 的具體實現(xiàn),主要是使用反射特性:
func (dc *DataContext) Add(key string, obj interface{}) {
dc.lockBase.Lock()
defer dc.lockBase.Unlock()
dc.base[key] = reflect.ValueOf(obj)
}
gengine 解析規(guī)則時會將自定義對象標記為variable類型,通過 GetValue() 獲取自定義對象屬性值:
// 獲取變量值
func (dc *DataContext) GetValue(Vars map[string]reflect.Value, variable string) (reflect.Value, error) {
if strings.Contains(variable, ".") {
// 對象a.b
structAndField := strings.Split(variable, ".")
if len(structAndField) == 2 {
a := structAndField[0]
b := structAndField[1]
// 獲取注入的對象
dc.lockBase.Lock()
v, ok := dc.base[a]
dc.lockBase.Unlock()
if ok {
return core.GetStructAttributeValue(v, b)
}
}
}
}
// 反射獲取對象屬性值
func GetStructAttributeValue(obj reflect.Value, fieldName string) (reflect.Value, error) {
stru := obj
var attrVal reflect.Value
if stru.Kind() == reflect.Ptr {
attrVal = stru.Elem().FieldByName(fieldName)
} else {
attrVal = stru.FieldByName(fieldName)
}
return attrVal, nil
}
支持自定義方法注入
同樣在上下文中注入自定義方法后,也可以在規(guī)則中使用注入的方法。使用例子:
// 規(guī)則體
rule "test-func" "測試自定義方法" salience 10
begin
// 自定義方法GetCount獲取指標數(shù)據(jù)(患者當天的訂單數(shù)量)
num = GetCount("order-patient-id", Order.PatientId)
if num >= 5 {
return
}
end
// 注入自定義方法GetCount
dataSvc := s.indicatorDao.NewDataService(ctx)
dataContext := gctx.NewDataContext()
dataContext.Add("GetCount", dataSvc.GetCount)
gengine 自定義方法的注入也是使用反射來實現(xiàn),自定義方法的注入同自定義對象一樣也是使用 Add() 方法注入。
gengine 解析規(guī)則時會將自定義方法標記為functionCall類型:
func (dc *DataContext) ExecFunc(Vars map[string]reflect.Value, funcName string, parameters []reflect.Value) (reflect.Value, error) {
// 獲取注入的方法
dc.lockBase.Lock()
v, ok := dc.base[funcName]
dc.lockBase.Unlock()
if ok {
args := core.ParamsTypeChange(v, parameters)
// 調用方法
res := v.Call(args)
raw, e := core.GetRawTypeValue(res)
if e != nil {
return reflect.ValueOf(nil), e
}
return raw, nil
}
}
支持并發(fā)執(zhí)行
通常情況下順序模式執(zhí)行即可滿足要求,但是當規(guī)則數(shù)量比較大時,順序執(zhí)行的耗時就會比較長。

規(guī)則引擎在執(zhí)行所有規(guī)則的時候,其實是遍歷全局的 hashmap 然后再順序執(zhí)行每一個規(guī)則,由于每個規(guī)則之間沒有依賴關系,因此可以用每一個規(guī)則一個協(xié)程來并發(fā)執(zhí)行。
func (g *Gengine) ExecuteConcurrent(rb *builder.RuleBuilder) error {
var wg sync.WaitGroup
wg.Add(len(rb.Kc.RuleEntities))
for _, r := range rb.Kc.RuleEntities {
rr := r
// 協(xié)程并發(fā)
go func() {
v, e, bx := rr.Execute(rb.Dc)
if bx {
g.addResult(rr.RuleName, v)
}
wg.Done()
}()
}
wg.Wait()
// 省略部分
}
使用場景
有了規(guī)則引擎之后,很多在業(yè)務代碼中的 if-else、switch 硬編碼,都能抽象為規(guī)則并使用規(guī)則引擎,這樣能通過配置規(guī)則代替硬編碼,能極大地縮短變更上線時間。
業(yè)務風控
通過業(yè)務數(shù)據(jù)分析,可以抽象出用戶異常行為的規(guī)則:

然后,風控系統(tǒng)在判斷是否為風險操作時,只需要規(guī)則引擎加載并執(zhí)行風控規(guī)則,即可得到結果。想要提高風控系統(tǒng)的準確性,只需要不斷地迭代完善風控規(guī)則。

規(guī)則引擎在業(yè)務風控的實踐,可以參考 基于準實時規(guī)則引擎的業(yè)務風控實踐。
運營活動
拿最常見的抽獎和做任務 2 種運營活動來說,都可以將具體活動邏輯抽象為業(yè)務規(guī)則:① 抽獎,不同的人&不同的場景對應不同的獎池(中獎概率與獎品集合規(guī)則);② 做任務,任務領取規(guī)則、任務完成指標動態(tài)可配(任務規(guī)則);

內容分發(fā)
針對某些特定的用戶或者某種場景的用戶,下發(fā)特定的展示內容或者推送短信等觸達消息,都可以將這些特定用戶的邏輯梳理為內容分發(fā)規(guī)則。

以上就是詳解如何使用Golang實現(xiàn)自定義規(guī)則引擎的詳細內容,更多關于Golang自定義規(guī)則引擎的資料請關注腳本之家其它相關文章!
相關文章
使用go實現(xiàn)刪除sql里面的注釋和字符串功能(demo)
這篇文章主要介紹了使用go實現(xiàn)刪除sql里面的注釋和字符串功能,本文通過實例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-11-11

