Golang 规则引擎原理及实战
本文主要介紹規則引擎在 golang 中的使用,將首先介紹 golang 中主要的規則引擎框架,然后利用 golang 原生的 parser 搭建一個簡單的規則引擎實現基本的 bool 表達式解析工作。
背景
隨著業務代碼的不斷迭代,誕生出了越來越多的 if-else,并且 if-else 中的邏輯越來越復雜,導致代碼邏輯復雜、維護性差、可讀性差、修改風險高等缺陷。
復雜的 if-else 邏輯其實對應的是一條條的規則,滿足對應的規則在執行對應的操作,即 if-else 中的條件就是一個對應的 bool 表達式:
|--bool 表達式--| if a == 1 && b == 2 {// do some business }一個復雜的邏輯表示一條對應的規則,將這些規則用 bool 表達式表示之后,便可以按照對應的規則執行操作,大大減少 if-else 的應用:
if 規則 {// do some business }而如何解析這些 bool 表達式便是規則引擎所需要完成的任務了。
規則引擎介紹
規則引擎由是一種嵌入在應用程序中的組件,實現了將業務決策從應用程序代碼中分離出來,并使用預定義的語義模塊編寫業務決策。
Golang語言實現的主要規則引擎框架:
| YQL(Yet another-Query-Language) | https://github.com/caibirdme/yql | 類SQL | 表達式解析 | 低 |
| govaluate | https://github.com/Knetic/govaluate | 類Golang | 表達式解析 | 低 |
| Gval | https://github.com/PaesslerAG/gval | 類Golang | 表達式解析 | 低 |
| Grule-Rule-Engine | https://github.com/hyperjumptech/grule-rule-engine | 自定義DSL(Domain-Specific Language) | 規則執行 | 中 |
| Gengine | https://github.com/bilibili/gengine | 自定義DSL(Domain-Specific Language) | 規則執行 | 中 |
| Common Expression Language | https://github.com/google/cel-go#evaluate | 類C | 通用表達式語言 | 中 |
| goja | https://github.com/dop251/goja | JavaScript | 規則解析 | 中 |
| GopherLua: VM and compiler for Lua in Go. | https://github.com/yuin/gopher-lua | lua | 規則解析 | 高 |
可以看到無數的人在前仆后繼地造規則引擎,但是這些規則引擎由于功能強大,因此對于一些比較簡單的邏輯表達式的解析任務來說就顯得有點重了。
比如想使用規則引擎實現如下的規則,例如如上的這些框架來實現解析的話會大量消耗 CPU 的資源,在請求量較大的系統當中就有可能成為系統的性能屏障。
if type == 1 && product_id = 3{//... }因此需要一個簡單輕便性能較好的規則引擎。
基于Go parser庫打造規則引擎
parser 庫介紹
Go 內置的 parser 庫提供了 golang 底層語法分析的相關操作,并且其相關的 api 向用戶開放,那么便可以直接使用 Go 的內置?parser 庫?完成上面一個基本規則引擎的框架。
針對如下的規則表達式使用go原生的parser進行解析(規則中不能使用 type 關鍵字):
// 使用go語法表示的bool表達式,in_array為函數調用 expr := `product_id == "3" && order_type == "0" && in_array(capacity_level, []string{"900","1100"}) && carpool_type == "0"`// 使用go parser解析上述表達式,返回結果為一顆ast parseResult, err := parser.ParseExpr(expr) if err != nil {fmt.Println(err)return }// 打印該ast ast.Print(nil, parseResult)可以得到如下的結果(一顆二叉樹):
0 *ast.BinaryExpr { 1 . X: *ast.BinaryExpr { 2 . . X: *ast.BinaryExpr { 3 . . . X: *ast.BinaryExpr { 4 . . . . X: *ast.Ident { 5 . . . . . NamePos: 1 6 . . . . . Name: "product_id" 7 . . . . } 8 . . . . OpPos: 12 9 . . . . Op: == 10 . . . . Y: *ast.BasicLit { 11 . . . . . ValuePos: 15 12 . . . . . Kind: STRING 13 . . . . . Value: "\"3\"" 14 . . . . } 15 . . . } 16 . . . OpPos: 19 17 . . . Op: && 18 . . . Y: *ast.BinaryExpr { 19 . . . . X: *ast.Ident { 20 . . . . . NamePos: 22 21 . . . . . Name: "order_type" 22 . . . . } 23 . . . . OpPos: 33 24 . . . . Op: == 25 . . . . Y: *ast.BasicLit { 26 . . . . . ValuePos: 36 27 . . . . . Kind: STRING 28 . . . . . Value: "\"0\"" 29 . . . . } 30 . . . } 31 . . } 32 . . OpPos: 40 33 . . Op: && 34 . . Y: *ast.CallExpr { 35 . . . Fun: *ast.Ident { 36 . . . . NamePos: 43 37 . . . . Name: "in_array" 38 . . . } 39 . . . Lparen: 51 40 . . . Args: []ast.Expr (len = 2) { 41 . . . . 0: *ast.Ident { 42 . . . . . NamePos: 52 43 . . . . . Name: "capacity_level" 44 . . . . } 45 . . . . 1: *ast.CompositeLit { 46 . . . . . Type: *ast.ArrayType { 47 . . . . . . Lbrack: 68 48 . . . . . . Elt: *ast.Ident { 49 . . . . . . . NamePos: 70 50 . . . . . . . Name: "string" 51 . . . . . . } 52 . . . . . } 53 . . . . . Lbrace: 76 54 . . . . . Elts: []ast.Expr (len = 2) { 55 . . . . . . 0: *ast.BasicLit { 56 . . . . . . . ValuePos: 77 57 . . . . . . . Kind: STRING 58 . . . . . . . Value: "\"900\"" 59 . . . . . . } 60 . . . . . . 1: *ast.BasicLit { 61 . . . . . . . ValuePos: 83 62 . . . . . . . Kind: STRING 63 . . . . . . . Value: "\"1100\"" 64 . . . . . . } 65 . . . . . } 66 . . . . . Rbrace: 89 67 . . . . . Incomplete: false 68 . . . . } 69 . . . } 70 . . . Ellipsis: 0 71 . . . Rparen: 90 72 . . } 73 . } 74 . OpPos: 92 75 . Op: && 76 . Y: *ast.BinaryExpr { 77 . . X: *ast.Ident { 78 . . . NamePos: 95 79 . . . Name: "carpool_type" 80 . . } 81 . . OpPos: 108 82 . . Op: == 83 . . Y: *ast.BasicLit { 84 . . . ValuePos: 111 85 . . . Kind: STRING 86 . . . Value: "\"0\"" 87 . . } 88 . } 89 }打造基于parser庫的規則引擎
將 parser 解析出來的這顆二叉樹畫出來:
可以看到,有了 Golang 原生的語法解析器,我們只需要后序遍歷這棵二叉樹,然后實現一套 AST 與對應數據map的映射關系即可實現一個簡單的規則引擎。
其中,AST 與對應數據map的映射關系的實現代碼的主要結構如下:
func eval(expr ast.Expr, data map[string]interface{}) interface{} {switch expr := expr.(type) {case *ast.BasicLit: // 匹配到數據return getlitValue(expr)case *ast.BinaryExpr: // 匹配到子樹// 后序遍歷x := eval(expr.X, data) // 左子樹結果y := eval(expr.Y, data) // 右子樹結果if x == nil || y == nil {return errors.New(fmt.Sprintf("%+v, %+v is nil", x, y))}op := expr.Op // 運算符// 按照不同類型執行運算switch x.(type) {case int64:return calculateForInt(x, y, op)case bool:return calculateForBool(x, y, op)case string:return calculateForString(x, y, op)case error:return errors.New(fmt.Sprintf("%+v %+v %+v eval failed", x, op, y))default:return errors.New(fmt.Sprintf("%+v op is not support", op))}case *ast.CallExpr: // 匹配到函數return calculateForFunc(expr.Fun.(*ast.Ident).Name, expr.Args, data)case *ast.ParenExpr: // 匹配到括號return eval(expr.X, data)case *ast.Ident: // 匹配到變量return data[expr.Name]default:return errors.New(fmt.Sprintf("%x type is not support", expr))} }完整的實現代碼在這里:go_parser
性能對比
使用基于 go parser 實現的規則引擎對比其他常見的規則引擎(YQL、govaluate、gval)的性能:
BenchmarkGoParser_Match-8 127189 8912 ns/op // 基于 go parser 實現的規則引擎 BenchmarkGval_Match-8 63584 18358 ns/op // gval BenchmarkGovaluateParser_Match-8 13628 86955 ns/op // govaluate BenchmarkYqlParser_Match-8 10364 112481 ns/op // yql總結
可以看到在使用原生的 parser 實現的規則引擎在性能上具有較大的優勢,但缺點在于需要自己實現一套 AST 與對應數據map的映射關系,并且受限于 go 原生 parser 庫的限制導致規則的定義語言比較繁瑣,這些也都是為什么會有其他規則引擎框架誕生的原因,但不可否認基于原生 parser 庫打造的規則引擎的性能還是足夠優秀的,因此在一些比較簡單的規則匹配場景中還是優先考慮使用原生 parser,可以最大效率的實現降本增效的效果。
?
創作挑戰賽新人創作獎勵來咯,堅持創作打卡瓜分現金大獎總結
以上是生活随笔為你收集整理的Golang 规则引擎原理及实战的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 基于 Go 的内置 Parser 打造轻
- 下一篇: 规则引擎:大厂营销系统资格设计全解