Golang 规则引擎原理及实战
本文主要介紹規(guī)則引擎在 golang 中的使用,將首先介紹 golang 中主要的規(guī)則引擎框架,然后利用 golang 原生的 parser 搭建一個(gè)簡單的規(guī)則引擎實(shí)現(xiàn)基本的 bool 表達(dá)式解析工作。
背景
隨著業(yè)務(wù)代碼的不斷迭代,誕生出了越來越多的 if-else,并且 if-else 中的邏輯越來越復(fù)雜,導(dǎo)致代碼邏輯復(fù)雜、維護(hù)性差、可讀性差、修改風(fēng)險(xiǎn)高等缺陷。
復(fù)雜的 if-else 邏輯其實(shí)對(duì)應(yīng)的是一條條的規(guī)則,滿足對(duì)應(yīng)的規(guī)則在執(zhí)行對(duì)應(yīng)的操作,即 if-else 中的條件就是一個(gè)對(duì)應(yīng)的 bool 表達(dá)式:
|--bool 表達(dá)式--| if a == 1 && b == 2 {// do some business }一個(gè)復(fù)雜的邏輯表示一條對(duì)應(yīng)的規(guī)則,將這些規(guī)則用 bool 表達(dá)式表示之后,便可以按照對(duì)應(yīng)的規(guī)則執(zhí)行操作,大大減少 if-else 的應(yīng)用:
if 規(guī)則 {// do some business }而如何解析這些 bool 表達(dá)式便是規(guī)則引擎所需要完成的任務(wù)了。
規(guī)則引擎介紹
規(guī)則引擎由是一種嵌入在應(yīng)用程序中的組件,實(shí)現(xiàn)了將業(yè)務(wù)決策從應(yīng)用程序代碼中分離出來,并使用預(yù)定義的語義模塊編寫業(yè)務(wù)決策。
Golang語言實(shí)現(xiàn)的主要規(guī)則引擎框架:
| YQL(Yet another-Query-Language) | https://github.com/caibirdme/yql | 類SQL | 表達(dá)式解析 | 低 |
| govaluate | https://github.com/Knetic/govaluate | 類Golang | 表達(dá)式解析 | 低 |
| Gval | https://github.com/PaesslerAG/gval | 類Golang | 表達(dá)式解析 | 低 |
| Grule-Rule-Engine | https://github.com/hyperjumptech/grule-rule-engine | 自定義DSL(Domain-Specific Language) | 規(guī)則執(zhí)行 | 中 |
| Gengine | https://github.com/bilibili/gengine | 自定義DSL(Domain-Specific Language) | 規(guī)則執(zhí)行 | 中 |
| Common Expression Language | https://github.com/google/cel-go#evaluate | 類C | 通用表達(dá)式語言 | 中 |
| goja | https://github.com/dop251/goja | JavaScript | 規(guī)則解析 | 中 |
| GopherLua: VM and compiler for Lua in Go. | https://github.com/yuin/gopher-lua | lua | 規(guī)則解析 | 高 |
可以看到無數(shù)的人在前仆后繼地造規(guī)則引擎,但是這些規(guī)則引擎由于功能強(qiáng)大,因此對(duì)于一些比較簡單的邏輯表達(dá)式的解析任務(wù)來說就顯得有點(diǎn)重了。
比如想使用規(guī)則引擎實(shí)現(xiàn)如下的規(guī)則,例如如上的這些框架來實(shí)現(xiàn)解析的話會(huì)大量消耗 CPU 的資源,在請(qǐng)求量較大的系統(tǒng)當(dāng)中就有可能成為系統(tǒng)的性能屏障。
if type == 1 && product_id = 3{//... }因此需要一個(gè)簡單輕便性能較好的規(guī)則引擎。
基于Go parser庫打造規(guī)則引擎
parser 庫介紹
Go 內(nèi)置的 parser 庫提供了 golang 底層語法分析的相關(guān)操作,并且其相關(guān)的 api 向用戶開放,那么便可以直接使用 Go 的內(nèi)置?parser 庫?完成上面一個(gè)基本規(guī)則引擎的框架。
針對(duì)如下的規(guī)則表達(dá)式使用go原生的parser進(jìn)行解析(規(guī)則中不能使用 type 關(guān)鍵字):
// 使用go語法表示的bool表達(dá)式,in_array為函數(shù)調(diào)用 expr := `product_id == "3" && order_type == "0" && in_array(capacity_level, []string{"900","1100"}) && carpool_type == "0"`// 使用go parser解析上述表達(dá)式,返回結(jié)果為一顆ast parseResult, err := parser.ParseExpr(expr) if err != nil {fmt.Println(err)return }// 打印該ast ast.Print(nil, parseResult)可以得到如下的結(jié)果(一顆二叉樹):
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庫的規(guī)則引擎
將 parser 解析出來的這顆二叉樹畫出來:
可以看到,有了 Golang 原生的語法解析器,我們只需要后序遍歷這棵二叉樹,然后實(shí)現(xiàn)一套 AST 與對(duì)應(yīng)數(shù)據(jù)map的映射關(guān)系即可實(shí)現(xiàn)一個(gè)簡單的規(guī)則引擎。
其中,AST 與對(duì)應(yīng)數(shù)據(jù)map的映射關(guān)系的實(shí)現(xiàn)代碼的主要結(jié)構(gòu)如下:
func eval(expr ast.Expr, data map[string]interface{}) interface{} {switch expr := expr.(type) {case *ast.BasicLit: // 匹配到數(shù)據(jù)return getlitValue(expr)case *ast.BinaryExpr: // 匹配到子樹// 后序遍歷x := eval(expr.X, data) // 左子樹結(jié)果y := eval(expr.Y, data) // 右子樹結(jié)果if x == nil || y == nil {return errors.New(fmt.Sprintf("%+v, %+v is nil", x, y))}op := expr.Op // 運(yùn)算符// 按照不同類型執(zhí)行運(yùn)算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: // 匹配到函數(shù)return calculateForFunc(expr.Fun.(*ast.Ident).Name, expr.Args, data)case *ast.ParenExpr: // 匹配到括號(hào)return eval(expr.X, data)case *ast.Ident: // 匹配到變量return data[expr.Name]default:return errors.New(fmt.Sprintf("%x type is not support", expr))} }完整的實(shí)現(xiàn)代碼在這里:go_parser
性能對(duì)比
使用基于 go parser 實(shí)現(xiàn)的規(guī)則引擎對(duì)比其他常見的規(guī)則引擎(YQL、govaluate、gval)的性能:
BenchmarkGoParser_Match-8 127189 8912 ns/op // 基于 go parser 實(shí)現(xiàn)的規(guī)則引擎 BenchmarkGval_Match-8 63584 18358 ns/op // gval BenchmarkGovaluateParser_Match-8 13628 86955 ns/op // govaluate BenchmarkYqlParser_Match-8 10364 112481 ns/op // yql總結(jié)
可以看到在使用原生的 parser 實(shí)現(xiàn)的規(guī)則引擎在性能上具有較大的優(yōu)勢,但缺點(diǎn)在于需要自己實(shí)現(xiàn)一套 AST 與對(duì)應(yīng)數(shù)據(jù)map的映射關(guān)系,并且受限于 go 原生 parser 庫的限制導(dǎo)致規(guī)則的定義語言比較繁瑣,這些也都是為什么會(huì)有其他規(guī)則引擎框架誕生的原因,但不可否認(rèn)基于原生 parser 庫打造的規(guī)則引擎的性能還是足夠優(yōu)秀的,因此在一些比較簡單的規(guī)則匹配場景中還是優(yōu)先考慮使用原生 parser,可以最大效率的實(shí)現(xiàn)降本增效的效果。
?
創(chuàng)作挑戰(zhàn)賽新人創(chuàng)作獎(jiǎng)勵(lì)來咯,堅(jiān)持創(chuàng)作打卡瓜分現(xiàn)金大獎(jiǎng)總結(jié)
以上是生活随笔為你收集整理的Golang 规则引擎原理及实战的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 基于 Go 的内置 Parser 打造轻
- 下一篇: golang常用库:字段参数验证库-va