基於 go 的規則引擎
【導讀】介紹 go 語言框架下開源規則引擎
1 引入
以一個電商運維場景爲例,我們需要對用戶註冊年限 p1、購買金額 p2、地域 p3 等條件給用戶進行發券,基於條件進行任意組合成不同規則。比如:
-
規則 1 :
p1 > 2 && p2 > 10 000 & p3 in (‘beijng’,’shanghai’)
大於 2 年的老用戶,並且購買金額大於 10000 的北京或上海用戶。 -
規則 2:
p1<1
小於 1 年的用戶
爲了解決這個問題,引入了規則引擎,從 if …else 中解放出來。Drools 是 java 語言的規則引擎,本文是針對 go 語言的規則引擎框架。
Snip2020110249
2 Go 開源
先說結論:比較了 govaluate、goengine、gorule,最終使用 govaluate。相比 gorule、goengine,govaluate 除了支持 in 操作、還支持正則表達式,而且表達式也不需要轉換成 DRL。
-
支持 string 類型的 == 操作
-
支持 in 操作
-
支持計算邏輯表達式和算數表達式
-
支持正則
2.1 govaluate
demo 代碼,需要生成一個 map 傳遞變量的值。如下是計算算數公式和邏輯表達式例子。
func TestGoValueate() {
// 支持多個邏輯表達式
expr, err := govaluate.NewEvaluableExpression("(10 > 0) && (2.1 == 2.1) && 'service is ok' == 'service is ok'" +
" && 1 in (1,2) && 'code1' in ('code3','code2',1)")
if err != nil {
log.Fatal("syntax error:", err)
}
result, err := expr.Evaluate(nil)
if err != nil {
log.Fatal("evaluate error:", err)
}
fmt.Println(result)
// 邏輯表達式包含變量
expression, err := govaluate.NewEvaluableExpression("http_response_body == 'service is ok'")
parameters := make(map[string]interface{}, 8)
parameters["http_response_body"] = "service is ok"
res, _ := expression.Evaluate(parameters)
fmt.Println(res)
// 算數表達式包含變量
expression1, _ := govaluate.NewEvaluableExpression("requests_made * requests_succeeded / 100")
parameters1 := make(map[string]interface{}, 8)
parameters1["requests_made"] = 100
parameters1["requests_succeeded"] = 80
result1, _ := expression1.Evaluate(parameters1)
fmt.Println(result1)
}
2、基準測試
func BenchmarkNewEvaluableExpression(b *testing.B) {
for i := 0; i < b.N; i++ {
_, err := govaluate.NewEvaluableExpression("(10 > 0) && (100 > 20) && 'code1' in ('code3','code2',1)")
if err != nil {
log.Fatal("syntax error:", err)
}
}
}
func BenchmarkEvaluate(b *testing.B) {
parameters1 := make(map[string]interface{}, 8)
parameters1["gmv"] = 100
parameters1["customerId"] = "80"
parameters1["stayLength"] = 20
for i := 0; i < b.N; i++ {
_, err := govaluate.NewEvaluableExpression("(gmv > 0) && (stayLength > 20) && customerId in ('80','code2','code3')")
if err != nil {
log.Fatal("syntax error:", err)
}
}
}
在測試 go 文件目錄下執行:
test go test -bench=. -benchmem
測試結果如下,每次執行 op 需要 15ms、9KB、需要內存分片次數 140 次。
goos: darwin
goarch: amd64
pkg: helloWord/test
BenchmarkNewEvaluableExpression-8 74413 15341 ns/op 8680 B/op 139 allocs/op
BenchmarkEvaluate-8
2.2 goengine 代碼
demo 如下:
import (
"fmt"
"gengine/builder"
"gengine/context"
"gengine/engine"
"github.com/google/martian/log"
"time"
)
func PrintName(name string) {
fmt.Println(name)
}
/**
use '@name',you can get rule name in rule content
*/
const atname_rule = `
rule "測試規則名稱1" "rule desc"
begin
va = @name
PrintName(va)
PrintName(@name)
end
rule "rule name" "rule desc"
begin
va = @name
PrintName(va)
PrintName(@name)
end
`
func TestGEngine() {
start1 := time.Now().UnixNano()
// context data
dataContext := context.NewDataContext()
dataContext.Add("PrintName", PrintName)
// init rule engine
ruleBuilder := builder.NewRuleBuilder(dataContext)
// resolve rules from string
err := ruleBuilder.BuildRuleFromString(atname_rule)
end1 := time.Now().UnixNano()
fmt.Println("rules num:%d, load rules cost time:%d ns", len(ruleBuilder.Kc.RuleEntities), end1-start1)
if err != nil {
log.Errorf("err:%s ", err)
} else {
eng := engine.NewGengine()
start := time.Now().UnixNano()
// true: means when there are many rules, if one rule execute error,continue to execute rules after the occur error rule
err := eng.Execute(ruleBuilder, true)
end := time.Now().UnixNano()
if err != nil {
log.Errorf("execute rule error: %v", err)
}
log.Infof("execute rule cost %d ns", end-start)
}
}
2.3 gorule
demo 如下:
type RuleConditionContext struct {
NetAmount float32
Distance int32
Duration int32
Result bool
}
// DRL的規則
const duplicateRulesWithDiffSalience = `
rule DuplicateRule1 "Duplicate Rule 1" salience 5 {
when
(RuleConditionContext.Distance > 5000 && RuleConditionContext.Duration > 120) && (RuleConditionContext.Result == false)
Then
RuleConditionContext.Result=true;
}
`
// 理想的規則引擎應該是解析一個規則+contextData
// 1.目前規則是:
// (1)執行步驟
// step1:加載所有規則
// step2: 執行一個數據
// (2) 問題
// 問題1: 執行效率
// 問題2: 如果需要提前加載所有規則,那麼就需要考慮:規則量、動態改變規則,比如規則被修改或者新增規則場景。
func TestGruleEngine() {
//Given
ruleCondition := &RuleConditionContext{
Distance: 6000,
Duration: 121,
Result: false,
}
lib := ast.NewKnowledgeLibrary()
ruleBuiler := builder.NewRuleBuilder(lib)
ruleBuiler.BuildRuleFromResource("rule1", "1.0", pkg.NewBytesResource([]byte(duplicateRulesWithDiffSalience)))
kb := lib.NewKnowledgeBaseInstance("rule1", "1.0")
eng := engine.NewGruleEngine()
// 2.對於一個對象進行判斷是否滿足規則
// 2.1 構建一個規則條件對象
dctx := ast.NewDataContext()
dctx.Add("RuleConditionContext", ruleCondition)
eng.Execute(dctx, kb)
fmt.Println(ruleCondition.Result)
}
2.4 goja
demo 如下:
import (
"fmt"
"github.com/dop251/goja"
)
func TestGoja() {
const SCRIPT = `
var hasX = false;
var hasY = false;
for (var key in o) {
switch (key) {
case "x":
if (hasX) {
throw "Already have x";
}
hasX = true;
delete o.y;
break;
case "y":
if (hasY) {
throw "Already have y";
}
hasY = true;
delete o.x;
break;
default:
throw "Unexpected property: " + key;
}
}
hasX && !hasY || hasY && !hasX;
`
r := goja.New()
r.Set("o", map[string]interface{}{
"x": 40,
"y": 2,
})
v, err := r.RunString(SCRIPT)
if err != nil {
fmt.Println(err)
}
fmt.Println(v)
}
轉自: heartthinkdo.com/?p=3711
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/OH2KT9XiYrzjWnk4q81BGw