基于 Go 的内置 Parser 打造轻量级规则引擎
在公司內見到無數的人在前仆后繼地造規則引擎,起因比較簡單,drools 之類的東西是 Java 生態的東西,與 Go 血緣不合,商業規則引擎又大多超重量級,從零開始建設的系統使用起來有很高的學習成本。剛好可能也不是很想寫 CRUD,幾個人一拍即合,所以就又有了造輪子的師出之名。
要造一個規則引擎,說難實際上也不難。程序員們這時候撿起了學生時代的編譯原理書,抄起遞歸下降、 lex/yacc 或者再先進一點的 antlr 之類的 parser generator 就搞了起來。造的時候說不定還發現噢噢,大多數 parser generator 還有不支持左遞歸的問題,然后按照它支持的文法寫出的 parser 需要自己處理計算表達式的左結合問題,嗯,非常有成就感,不知道比 CRUD 高到哪里去了。
不過多久就寫出了一個誰也不是很好看懂的新輪子。
實際上要那么費勁嗎?顯然是不用的。被很多人選擇性忽略的事實是,Go 的 parser api 是直接暴露給用戶的。可能接下來你已經知道我要說什么了。
對的,你可以直接使用 Go 的內置 parser 庫完成上面一個基本規則引擎的框架。從功能上來講,規則引擎的基本就是一個 bool 表達式的解析和求值過程。bool 表達式是啥呢?很簡單:
|--bool 表達式--| if a == 1 && b == 2 {// do your business }你每天都在寫的無聊透頂的 if else 就是各種 bool 表達式啊。你別看他無聊,沒有 bool 表達式的話,任何程序都沒有辦法順利地組織其邏輯,也就沒有什么 control flow 一說了。
先寫一個簡單的 demo,來 parse 并打印上面代碼中的?a == 1 && b == 2:
package mainimport ("fmt""go/ast""go/parser""go/token" )func main() {expr := `a == 1 && b == 2`fset := token.NewFileSet()exprAst, err := parser.ParseExpr(expr)if err != nil {fmt.Println(err)return}ast.Print(fset, exprAst) }湊合看看,bool 邏輯一般解析后就是最最簡單的 AST:
0 *ast.BinaryExpr {1 . X: *ast.BinaryExpr {2 . . X: *ast.Ident {3 . . . NamePos: -4 . . . Name: "a"5 . . . Obj: *ast.Object {6 . . . . Kind: bad7 . . . . Name: ""8 . . . }9 . . }10 . . OpPos: -11 . . Op: ==12 . . Y: *ast.BasicLit {13 . . . ValuePos: -14 . . . Kind: INT15 . . . Value: "1"16 . . }17 . }18 . OpPos: -19 . Op: &&20 . Y: *ast.BinaryExpr {21 . . X: *ast.Ident {22 . . . NamePos: -23 . . . Name: "b"24 . . . Obj: *(obj @ 5)25 . . }26 . . OpPos: -27 . . Op: ==28 . . Y: *ast.BasicLit {29 . . . ValuePos: -30 . . . Kind: INT31 . . . Value: "2"32 . . }33 . }34 }這種 AST 實在太常見了以致于我都不是很想解釋。。。大多數存儲系統的查詢 DSL 部分都會有 bool 表達式的痕跡,比如 Elasticsearch,SQL 語句的 where 等等,兩年前我曾經造過一個把 SQL 和 Elasticsearch 的 DSL 互相轉換的輪子,當時還寫了篇文章講了講原理:Day4: 《將sql轉換為es的DSL》 - Elastic 中文社區?。
Elasticsearch 在 7.0 的 xpack 中已經開始漸漸支持 SQL 功能了,所以這個輪子慢慢地也就變成了時代的眼淚。
眼淚歸眼淚,這種“邏輯”上的“是”或者“否”的判斷表達式,都是可以互相對應的,不管哪類的系統,誰設計的多么丑陋的 DSL,大抵上都是可以通過簡單的 (field op value) and/or 連接并且有括號的基本表達式來表達的。為啥還有這么多亂七八糟的 DSL?我想了想,基本的原因有三個:
仔細看看,主觀的因素兩個,客觀的因素是 bool 表達式擴展能力不強。嗯,我們來想想,比較典型的 bool 表達式場景:SQL 的表達能力不強嗎?普通需求滿足不了時,SQL 是怎么進行擴展的呢?
答案其實也挺簡單,SQL 的功能可以通過函數來進行擴展,比如 SQL 里支持 group_concat、date_sub 之類的函數,也支持一些簡單的 ETL 功能,比如 from_unixtime,unix_timestamp 等等。這一點,在本文開頭提出的使用 Go 內部 parser 來實現的規則引擎中可以支持么?
顯然你在 Go 里也寫過這種 if 判斷里有函數調用的邏輯:
func main() {expr := `a == 1 && b == 2 && in_array(c, []int{1,2,3,4})`fset := token.NewFileSet()exprAst, err := parser.ParseExpr(expr)if err != nil {fmt.Println(err)return}ast.Print(fset, exprAst) }輸出內容:
0 *ast.BinaryExpr {1 . X: *ast.BinaryExpr {2 . . X: *ast.BinaryExpr {3 . . . X: *ast.Ident {4 . . . . NamePos: -5 . . . . Name: "a"6 . . . . Obj: *ast.Object {7 . . . . . Kind: bad8 . . . . . Name: ""9 . . . . }10 . . . }11 . . . OpPos: -12 . . . Op: ==13 . . . Y: *ast.BasicLit {14 . . . . ValuePos: -15 . . . . Kind: INT16 . . . . Value: "1"17 . . . }18 . . }19 . . OpPos: -20 . . Op: &&21 . . Y: *ast.BinaryExpr {22 . . . X: *ast.Ident {23 . . . . NamePos: -24 . . . . Name: "b"25 . . . . Obj: *(obj @ 6)26 . . . }27 . . . OpPos: -28 . . . Op: ==29 . . . Y: *ast.BasicLit {30 . . . . ValuePos: -31 . . . . Kind: INT32 . . . . Value: "2"33 . . . }34 . . }35 . }36 . OpPos: -37 . Op: &&38 . Y: *ast.CallExpr {39 . . Fun: *ast.Ident {40 . . . NamePos: -41 . . . Name: "in_array"42 . . . Obj: *(obj @ 6)43 . . }44 . . Lparen: -45 . . Args: []ast.Expr (len = 2) {46 . . . 0: *ast.Ident {47 . . . . NamePos: -48 . . . . Name: "c"49 . . . . Obj: *(obj @ 6)50 . . . }51 . . . 1: *ast.CompositeLit {52 . . . . Type: *ast.ArrayType {53 . . . . . Lbrack: -54 . . . . . Elt: *ast.Ident {55 . . . . . . NamePos: -56 . . . . . . Name: "int"57 . . . . . . Obj: *(obj @ 6)58 . . . . . }59 . . . . }60 . . . . Lbrace: -61 . . . . Elts: []ast.Expr (len = 4) {62 . . . . . 0: *ast.BasicLit {63 . . . . . . ValuePos: -64 . . . . . . Kind: INT65 . . . . . . Value: "1"66 . . . . . }67 . . . . . 1: *ast.BasicLit {68 . . . . . . ValuePos: -69 . . . . . . Kind: INT70 . . . . . . Value: "2"71 . . . . . }72 . . . . . 2: *ast.BasicLit {73 . . . . . . ValuePos: -74 . . . . . . Kind: INT75 . . . . . . Value: "3"76 . . . . . }77 . . . . . 3: *ast.BasicLit {78 . . . . . . ValuePos: -79 . . . . . . Kind: INT80 . . . . . . Value: "4"81 . . . . . }82 . . . . }83 . . . . Rbrace: -84 . . . . Incomplete: false85 . . . }86 . . }87 . . Ellipsis: -88 . . Rparen: -89 . }90 }有了這些東西,在 parser 層面你要做的事情其實基本也就沒啥了。只不過需要簡單查查 Go 的語言 spec,看看 expression 到底支持哪些語法。
實在不是不得已,根本沒有必要造新的 DSL 和 parser。況且在一套生態里做出另一種奇怪的語言來,你不覺得別扭嗎?
當然,說歸說,業務系統中的 DSL 這種東西一般是給程序員來用的,或者可以用在兩個系統之間做交互,如果規則引擎的需求方是公司的運營人員或者業務人員,那么顯然用 DSL 是不合適的。更好的做法是為他們提供一套 GUI,然后把用戶點選的選項存儲下來。這時候用 json 更為合適,也不需要你去寫 parser 了。
你說你想自己造一個 json parser?
呵呵。
除了構造 AST,規則引擎剩下的工作就是在遍歷 AST 的時候,能返回 true 或者 false。其實就是簡單的 DFS,應屆生都會寫。
總結
以上是生活随笔為你收集整理的基于 Go 的内置 Parser 打造轻量级规则引擎的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Gengine规则引擎
- 下一篇: Golang 规则引擎原理及实战