詳解如何使用Golang實(shí)現(xiàn)自定義規(guī)則引擎
規(guī)則引擎的功能可以簡(jiǎn)化為當(dāng)滿足一些條件時(shí)觸發(fā)一些操作,通常使用 DSL 自定義語(yǔ)法來(lái)表述。規(guī)則引擎需要先解析 DSL 語(yǔ)法形成語(yǔ)法樹,然后遍歷語(yǔ)法樹得到完整的語(yǔ)法表達(dá)式,最后執(zhí)行這些語(yǔ)法表達(dá)式完成規(guī)則的執(zhí)行。
本文以gengine來(lái)探討如何設(shè)計(jì)和實(shí)現(xiàn)一個(gè)自定義規(guī)則引擎。
支持的語(yǔ)句
為了滿足基本的業(yè)務(wù)規(guī)則需求,規(guī)則引擎應(yīng)該要支持的語(yǔ)句有:
邏輯與算術(shù)運(yùn)算
- 數(shù)學(xué)運(yùn)算(+、-、*、/)
- 邏輯運(yùn)算(&&、||、!)
- 比較運(yùn)算(==、!=、>、<、>=、<=)
流程控制
- 條件(IF ELSE)
- 循環(huán) (FOR)
高級(jí)語(yǔ)句
- 對(duì)象屬性訪問(對(duì)象。屬性)
- 方法調(diào)用(func ())
規(guī)則語(yǔ)法的解析
規(guī)則的 DSL 語(yǔ)法定義應(yīng)該簡(jiǎn)單明了,gengine 使用了開源的語(yǔ)法解析器 Antlr4 來(lái)定義和解析規(guī)則語(yǔ)法。
定義規(guī)則語(yǔ)法
規(guī)則的 DSL 基本語(yǔ)法格式如下:
rule "rulename" "rule-describtion" salience 10 begin //規(guī)則體 end
其中規(guī)則體為具體規(guī)則語(yǔ)句,由上述的 邏輯與算術(shù)運(yùn)算、流程控制、高級(jí)語(yǔ)句 組合而成。
例如,判斷為一個(gè)大額異常訂單的規(guī)則體:
if Order.Price>= 1000000 {
return
}
編寫解析器語(yǔ)法
Antlr4 解析器語(yǔ)法定義文件后綴名為.g4,以下內(nèi)容為解析器的語(yǔ)法定義,解析器根據(jù)語(yǔ)法定義去逐行解析生成語(yǔ)法樹。
這里省略了一些非核心的語(yǔ)法定義并做了簡(jiǎn)化,完整內(nèi)容查看 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?;
// 基本語(yǔ)句
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;
解析器生成語(yǔ)法樹
如,判斷為一個(gè)大額異常訂單的規(guī)則:
rule "order-large-price" "訂單大額金額" salience 10
begin
if Order.Price >= 1000000 {
return
}
end
語(yǔ)法解析器解析之后,生成語(yǔ)法樹:

遍歷語(yǔ)法樹生成語(yǔ)句表達(dá)式
解析器生成語(yǔ)法樹之后,只需要遍歷語(yǔ)法樹即可得到完整的語(yǔ)句表達(dá)式。Antlr4 解析器會(huì)生成 Listener 接口,這些接口在遍歷語(yǔ)法樹時(shí)會(huì)被調(diào)用。
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)在遍歷語(yǔ)法樹時(shí),每個(gè)節(jié)點(diǎn)都有 EnterXXX() 和 ExitXXX() 方法存在,是成對(duì)出現(xiàn)的。
因此要遍歷語(yǔ)法樹只需要實(shí)現(xiàn) gengineListener 接口即可,gengine 巧妙的引入棧結(jié)構(gòu),遍歷完語(yǔ)法樹后(樹的遞歸遍歷就是進(jìn)棧出棧過程),就得到了完整的規(guī)則語(yǔ)句表達(dá)式。這里只列舉部分方法,完整實(shí)現(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ī)則內(nèi)容之后,規(guī)則的數(shù)據(jù)結(jié)構(gòu)如下:

全局的 hashmap 以規(guī)則名為 key,規(guī)則體為 value,規(guī)則體中的 ruleContent 為該規(guī)則所有的語(yǔ)句表達(dá)式列表,列表中的值指向具體的語(yǔ)句表達(dá)式實(shí)體,語(yǔ)句表達(dá)式實(shí)體由 邏輯與算術(shù)運(yùn)算、流程控制(IF、FOR)等基本語(yǔ)句組成。
規(guī)則語(yǔ)法的執(zhí)行
其實(shí)遍歷語(yǔ)法樹的過程中,將規(guī)則的執(zhí)行邏輯也放入 ExitXXX() 方法,這樣就能一并完成規(guī)則的解析和執(zhí)行。但是 gengine 沒有這么做,而是將規(guī)則的解析和執(zhí)行解耦,因?yàn)橐?guī)則的解析往往只需要初始化一次,或者在規(guī)則有變更時(shí)熱更新解析,而規(guī)則的執(zhí)行則是在需要校驗(yàn)規(guī)則的時(shí)候。
從 gengine 的規(guī)則數(shù)據(jù)結(jié)構(gòu)可知,只需要遍歷全局的 hashmap,即可按順序執(zhí)行所有的規(guī)則(順序模式),執(zhí)行每一個(gè)規(guī)則后會(huì)通過addResult()方法記錄執(zhí)行結(jié)果:
// 順序模式
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 {
// 記錄每個(gè)規(guī)則執(zhí)行結(jié)果
g.addResult(r.RuleName, v)
}
}
// 省略部分
...
}
對(duì)于某一個(gè)規(guī)則的執(zhí)行,則會(huì)去遍歷規(guī)則體 ruleContent 的所有語(yǔ)句表達(dá)式列表,然后按順序去執(zhí)行該規(guī)則下的所有語(yǔ)句表達(dá)式:
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 為每個(gè)語(yǔ)句類型都實(shí)現(xiàn)了 Evaluate() 方法,這里只討論 IF 語(yǔ)句的執(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í)行條件表達(dá)式
it, err := i.Expression.Evaluate(dc, Vars)
if err != nil {
return reflect.ValueOf(nil), err, false
}
// 執(zhí)行條件為真時(shí)的語(yǔ)句
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
}
其中條件表達(dá)式Expression.Evaluate()為計(jì)算條件表達(dá)式的值:
func (e *Expression) Evaluate(dc *context.DataContext, Vars map[string]reflect.Value) (reflect.Value, error) {
// 原子表達(dá)式
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 != "" {
// 計(jì)算左值
lv, err := e.ExpressionLeft.Evaluate(dc, Vars)
if err != nil {
return reflect.ValueOf(nil), err
}
// 計(jì)算右值
rv, err := e.ExpressionRight.Evaluate(dc, Vars)
if err != nil {
return reflect.ValueOf(nil), err
}
// 省略了類型轉(zhuǎn)化
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()原子表達(dá)式時(shí),就可以得到該原子表達(dá)式的值以結(jié)束遞歸:
func (e *ExpressionAtom) Evaluate(dc *context.DataContext, Vars map[string]reflect.Value) (reflect.Value, error) {
if len(e.Variable) > 0 {
// 是變量則取變量值,通過反射獲取注入的自定義對(duì)象值
return dc.GetValue(Vars, e.Variable)
} else if e.Constant != nil {
// 是常量就返回值
return e.Constant.Evaluate(dc, Vars)
}
// 省略部分
}
支持自定義對(duì)象注入
在上下文中注入自定義對(duì)象后,就可以在規(guī)則中使用注入的對(duì)象。使用例子:
// 規(guī)則體
rule "test-object" "測(cè)試自定義對(duì)象" salience 10
begin
// 訪問自定義對(duì)象Order
if Order.Price >= 1000000 {
return
}
end
// 注入自定義對(duì)象Order
dataContext := gctx.NewDataContext()
dataContext.Add("Order", Order)
現(xiàn)在來(lái)看下 gengine 的具體實(shí)現(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ī)則時(shí)會(huì)將自定義對(duì)象標(biāo)記為variable類型,通過 GetValue() 獲取自定義對(duì)象屬性值:
// 獲取變量值
func (dc *DataContext) GetValue(Vars map[string]reflect.Value, variable string) (reflect.Value, error) {
if strings.Contains(variable, ".") {
// 對(duì)象a.b
structAndField := strings.Split(variable, ".")
if len(structAndField) == 2 {
a := structAndField[0]
b := structAndField[1]
// 獲取注入的對(duì)象
dc.lockBase.Lock()
v, ok := dc.base[a]
dc.lockBase.Unlock()
if ok {
return core.GetStructAttributeValue(v, b)
}
}
}
}
// 反射獲取對(duì)象屬性值
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" "測(cè)試自定義方法" salience 10
begin
// 自定義方法GetCount獲取指標(biāo)數(shù)據(jù)(患者當(dāng)天的訂單數(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 自定義方法的注入也是使用反射來(lái)實(shí)現(xiàn),自定義方法的注入同自定義對(duì)象一樣也是使用 Add() 方法注入。
gengine 解析規(guī)則時(shí)會(huì)將自定義方法標(biāo)記為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)
// 調(diào)用方法
res := v.Call(args)
raw, e := core.GetRawTypeValue(res)
if e != nil {
return reflect.ValueOf(nil), e
}
return raw, nil
}
}
支持并發(fā)執(zhí)行
通常情況下順序模式執(zhí)行即可滿足要求,但是當(dāng)規(guī)則數(shù)量比較大時(shí),順序執(zhí)行的耗時(shí)就會(huì)比較長(zhǎng)。

規(guī)則引擎在執(zhí)行所有規(guī)則的時(shí)候,其實(shí)是遍歷全局的 hashmap 然后再順序執(zhí)行每一個(gè)規(guī)則,由于每個(gè)規(guī)則之間沒有依賴關(guān)系,因此可以用每一個(gè)規(guī)則一個(gè)協(xié)程來(lái)并發(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()
// 省略部分
}
使用場(chǎng)景
有了規(guī)則引擎之后,很多在業(yè)務(wù)代碼中的 if-else、switch 硬編碼,都能抽象為規(guī)則并使用規(guī)則引擎,這樣能通過配置規(guī)則代替硬編碼,能極大地縮短變更上線時(shí)間。
業(yè)務(wù)風(fēng)控
通過業(yè)務(wù)數(shù)據(jù)分析,可以抽象出用戶異常行為的規(guī)則:

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

規(guī)則引擎在業(yè)務(wù)風(fēng)控的實(shí)踐,可以參考 基于準(zhǔn)實(shí)時(shí)規(guī)則引擎的業(yè)務(wù)風(fēng)控實(shí)踐。
運(yùn)營(yíng)活動(dòng)
拿最常見的抽獎(jiǎng)和做任務(wù) 2 種運(yùn)營(yíng)活動(dòng)來(lái)說(shuō),都可以將具體活動(dòng)邏輯抽象為業(yè)務(wù)規(guī)則:① 抽獎(jiǎng),不同的人&不同的場(chǎng)景對(duì)應(yīng)不同的獎(jiǎng)池(中獎(jiǎng)概率與獎(jiǎng)品集合規(guī)則);② 做任務(wù),任務(wù)領(lǐng)取規(guī)則、任務(wù)完成指標(biāo)動(dòng)態(tài)可配(任務(wù)規(guī)則);

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

以上就是詳解如何使用Golang實(shí)現(xiàn)自定義規(guī)則引擎的詳細(xì)內(nèi)容,更多關(guān)于Golang自定義規(guī)則引擎的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Go語(yǔ)言fmt庫(kù)詳解與應(yīng)用實(shí)例(格式化輸入輸出功能)
fmt庫(kù)是Go語(yǔ)言中一個(gè)強(qiáng)大而靈活的庫(kù),提供了豐富的格式化輸入輸出功能,通過本文的介紹和實(shí)例演示,相信你對(duì)fmt庫(kù)的使用有了更深的理解,感興趣的朋友一起看看吧2023-10-10
Golang String字符串類型轉(zhuǎn)Json格式
本文主要介紹了Golang String字符串類型轉(zhuǎn)Json格式的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2024-05-05
深入了解Go語(yǔ)言中database/sql是如何設(shè)計(jì)的
在?Go?語(yǔ)言中內(nèi)置了?database/sql?包,它只對(duì)外暴露了一套統(tǒng)一的編程接口,便可以操作不同數(shù)據(jù)庫(kù),那么database/sql?是如何設(shè)計(jì)的呢,下面就來(lái)和大家簡(jiǎn)單聊聊吧2023-07-07
使用go實(shí)現(xiàn)刪除sql里面的注釋和字符串功能(demo)
這篇文章主要介紹了使用go實(shí)現(xiàn)刪除sql里面的注釋和字符串功能,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-11-11

