基于 Go 的内置 Parser 打造轻量级规则引擎
在公司內(nèi)見到無數(shù)的人在前仆后繼地造規(guī)則引擎,起因比較簡(jiǎn)單,drools 之類的東西是 Java 生態(tài)的東西,與 Go 血緣不合,商業(yè)規(guī)則引擎又大多超重量級(jí),從零開始建設(shè)的系統(tǒng)使用起來有很高的學(xué)習(xí)成本。剛好可能也不是很想寫 CRUD,幾個(gè)人一拍即合,所以就又有了造輪子的師出之名。
要造一個(gè)規(guī)則引擎,說難實(shí)際上也不難。程序員們這時(shí)候撿起了學(xué)生時(shí)代的編譯原理書,抄起遞歸下降、 lex/yacc 或者再先進(jìn)一點(diǎn)的 antlr 之類的 parser generator 就搞了起來。造的時(shí)候說不定還發(fā)現(xiàn)噢噢,大多數(shù) parser generator 還有不支持左遞歸的問題,然后按照它支持的文法寫出的 parser 需要自己處理計(jì)算表達(dá)式的左結(jié)合問題,嗯,非常有成就感,不知道比 CRUD 高到哪里去了。
不過多久就寫出了一個(gè)誰也不是很好看懂的新輪子。
實(shí)際上要那么費(fèi)勁嗎?顯然是不用的。被很多人選擇性忽略的事實(shí)是,Go 的 parser api 是直接暴露給用戶的。可能接下來你已經(jīng)知道我要說什么了。
對(duì)的,你可以直接使用 Go 的內(nèi)置 parser 庫(kù)完成上面一個(gè)基本規(guī)則引擎的框架。從功能上來講,規(guī)則引擎的基本就是一個(gè) bool 表達(dá)式的解析和求值過程。bool 表達(dá)式是啥呢?很簡(jiǎn)單:
|--bool 表達(dá)式--| if a == 1 && b == 2 {// do your business }你每天都在寫的無聊透頂?shù)?if else 就是各種 bool 表達(dá)式啊。你別看他無聊,沒有 bool 表達(dá)式的話,任何程序都沒有辦法順利地組織其邏輯,也就沒有什么 control flow 一說了。
先寫一個(gè)簡(jiǎn)單的 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 邏輯一般解析后就是最最簡(jiǎn)單的 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 實(shí)在太常見了以致于我都不是很想解釋。。。大多數(shù)存儲(chǔ)系統(tǒng)的查詢 DSL 部分都會(huì)有 bool 表達(dá)式的痕跡,比如 Elasticsearch,SQL 語句的 where 等等,兩年前我曾經(jīng)造過一個(gè)把 SQL 和 Elasticsearch 的 DSL 互相轉(zhuǎn)換的輪子,當(dāng)時(shí)還寫了篇文章講了講原理:Day4: 《將sql轉(zhuǎn)換為es的DSL》 - Elastic 中文社區(qū)?。
Elasticsearch 在 7.0 的 xpack 中已經(jīng)開始漸漸支持 SQL 功能了,所以這個(gè)輪子慢慢地也就變成了時(shí)代的眼淚。
眼淚歸眼淚,這種“邏輯”上的“是”或者“否”的判斷表達(dá)式,都是可以互相對(duì)應(yīng)的,不管哪類的系統(tǒng),誰設(shè)計(jì)的多么丑陋的 DSL,大抵上都是可以通過簡(jiǎn)單的 (field op value) and/or 連接并且有括號(hào)的基本表達(dá)式來表達(dá)的。為啥還有這么多亂七八糟的 DSL?我想了想,基本的原因有三個(gè):
仔細(xì)看看,主觀的因素兩個(gè),客觀的因素是 bool 表達(dá)式擴(kuò)展能力不強(qiáng)。嗯,我們來想想,比較典型的 bool 表達(dá)式場(chǎng)景:SQL 的表達(dá)能力不強(qiáng)嗎?普通需求滿足不了時(shí),SQL 是怎么進(jìn)行擴(kuò)展的呢?
答案其實(shí)也挺簡(jiǎn)單,SQL 的功能可以通過函數(shù)來進(jìn)行擴(kuò)展,比如 SQL 里支持 group_concat、date_sub 之類的函數(shù),也支持一些簡(jiǎn)單的 ETL 功能,比如 from_unixtime,unix_timestamp 等等。這一點(diǎn),在本文開頭提出的使用 Go 內(nèi)部 parser 來實(shí)現(xiàn)的規(guī)則引擎中可以支持么?
顯然你在 Go 里也寫過這種 if 判斷里有函數(shù)調(diào)用的邏輯:
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) }輸出內(nèi)容:
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 層面你要做的事情其實(shí)基本也就沒啥了。只不過需要簡(jiǎn)單查查 Go 的語言 spec,看看 expression 到底支持哪些語法。
實(shí)在不是不得已,根本沒有必要造新的 DSL 和 parser。況且在一套生態(tài)里做出另一種奇怪的語言來,你不覺得別扭嗎?
當(dāng)然,說歸說,業(yè)務(wù)系統(tǒng)中的 DSL 這種東西一般是給程序員來用的,或者可以用在兩個(gè)系統(tǒng)之間做交互,如果規(guī)則引擎的需求方是公司的運(yùn)營(yíng)人員或者業(yè)務(wù)人員,那么顯然用 DSL 是不合適的。更好的做法是為他們提供一套 GUI,然后把用戶點(diǎn)選的選項(xiàng)存儲(chǔ)下來。這時(shí)候用 json 更為合適,也不需要你去寫 parser 了。
你說你想自己造一個(gè) json parser?
呵呵。
除了構(gòu)造 AST,規(guī)則引擎剩下的工作就是在遍歷 AST 的時(shí)候,能返回 true 或者 false。其實(shí)就是簡(jiǎn)單的 DFS,應(yīng)屆生都會(huì)寫。
總結(jié)
以上是生活随笔為你收集整理的基于 Go 的内置 Parser 打造轻量级规则引擎的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Gengine规则引擎
- 下一篇: Golang 规则引擎原理及实战