golang规则引擎原理及实战(代码片段)

刘贤松handler 刘贤松handler     2023-04-02     439

关键词:

本文主要介绍规则引擎在 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表达式解析
govaluateGitHub - Knetic/govaluate: Arbitrary expression evaluation for golang类Golang表达式解析
GvalGitHub - PaesslerAG/gval: Expression evaluation in golang类Golang表达式解析
Grule-Rule-Enginehttps://github.com/hyperjumptech/grule-rule-engine自定义DSL(Domain-Specific Language)规则执行
GengineGitHub - bilibili/gengine自定义DSL(Domain-Specific Language)规则执行
Common Expression Languagehttps://github.com/google/cel-go#evaluate类C通用表达式语言
gojaGitHub - dop251/goja: ECMAScript/JavaScript engine in pure GoJavaScript规则解析
GopherLua: VM and compiler for Lua in Go.https://github.com/yuin/gopher-lualua规则解析

可以看到无数的人在前仆后继地造规则引擎,但是这些规则引擎由于功能强大,因此对于一些比较简单的逻辑表达式的解析任务来说就显得有点重了。

比如想使用规则引擎实现如下的规则,例如如上的这些框架来实现解析的话会大量消耗 CPU 的资源,在请求量较大的系统当中就有可能成为系统的性能屏障。

if type == 1 && a = 3
    //...

因此需要一个简单轻便性能较好的规则引擎。

基于Go parser库打造规则引擎

parser 库介绍

Go 内置的 parser 库提供了 golang 底层语法分析的相关操作,并且其相关的 api 向用户开放,那么便可以直接使用 Go 的内置 parser 库 完成上面一个基本规则引擎的框架。

针对如下的规则表达式使用go原生的parser进行解析(规则中不能使用 type 关键字):

// 使用go语法表示的bool表达式,in_array为函数调用
expr := `a == "3" && b == "0" && in_array(c, []string"900","1100") && d == "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: "a"
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: "b"
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: "c"
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: "d"
80  .  .  
81  .  .  OpPos: 108
82  .  .  Op: ==
83  .  .  Y: *ast.BasicLit 
84  .  .  .  ValuePos: 111
85  .  .  .  Kind: STRING
86  .  .  .  Value: "\\"0\\""
87  .  .  
88  .  
89  

打造基于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))
   

完整的实现代码在这里:gparser

性能对比

使用基于 go parser 实现的规则引擎对比其他常见的规则引擎(YQL、govaluate、gval)的性能:

BenchmarkGParser_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规则引擎原理及实战(代码片段)

本文主要介绍规则引擎在golang中的使用,将首先介绍golang中主要的规则引擎框架,然后利用golang原生的parser搭建一个简单的规则引擎实现基本的bool表达式解析工作。 背景随着业务代码的不断迭代,诞生出了越来越多... 查看详情

googleaviator——轻量级java表达式引擎实战(代码片段)

...擎技术及比较Drools简介Drools(JBossRules)是一个开源业务规则引擎,符合业内标准,速度快、效率高。业务分析师或审核人员可以利用它轻松查看业务规则,从而检验是否已编码的规则执行了所需的业务规则。除了应用了Rete核心... 查看详情

规则引擎easyrules介绍,应用及示例(代码片段)

浅析EasyRules规则引擎轻量级规则引擎EasyRulesJava规则引擎EasyRules一、EasyRules介绍1.1概述EasyRules是一个简单而强大的Java规则引擎,提供以下功能:轻量级框架和易于学习的API基于POJO的开发与注解的编程模型定义抽象的业务... 查看详情

规则引擎drools介绍使用及springboot整合drools(代码片段)

规则引擎Drools介绍、使用及SpringBoot整合Drools一、Drools介绍1.1优点1.2架构1.2.1Drools引擎的核心组件1.2.2规则引擎工作流程简述1.3Drools语法1.3.1规则文件1.3.1.1.规则详情1.属性详情2.条件部分-LHS3.结果部分-RHS1.3.1.2Drools关键词1.3.1.3Drools... 查看详情

java开源的规则引擎drools电商行业实战(含完整代码)(代码片段)

...量是动态的,另外考虑到扩展性,满足将来业务规则的增长,不只是限制领取数需要新加其他条件,为了满足不断变化的业务场景,经分析后选用规则引擎Drools来实现。本场Chat通过一个电商行业的领取优惠券... 查看详情

howjavascriptworks(javascript工作原理)渲染引擎及性能优化小技巧(代码片段)

...:读完这篇文章需要20分钟,这篇文章主要讲解了浏览器中引擎的渲染机制。DOMtree    ----|        |----> RenderTreeCSSOMtree ----| 这是JavaScript工作原理的第十一章。迄今为止,之前的JavaScript工作原... 查看详情

基于camunda开源流程引擎如何实现会签及会签原理解析(代码片段)

一、背景市场上比较有名的开源流程引擎有osworkflow、jbpm、activiti、flowable、camunda。由于jbpm、activiti、flowable这几个流程引擎出现的比较早,国内人用的比较多,大家对camunda流程引擎认识的不多,实际上camunda在功能上... 查看详情

规则引擎drools示例:个人所得税计算器信用卡申请保险产品准入规则(代码片段)

...一、Drools实战1.1个人所得税计算器1.1.1名词解释1.1.2计算规则1.1.3实现步骤1.2信用卡申请1.2.1计算规则1.2.2实现步骤1.3保险产品准入规则1.3.1决策表1.3.2规则介绍1.3.3实现步骤一、Drools实战1.1个人所得税计算器本小节我们需要通过Drool... 查看详情

python应用实战案例-pythongeopandas包详解(附大量案例及代码)(代码片段)

...行下载。深度学习100例全系列详细教程 深度学习算法原理介绍及应用案例tensorflow从入门到精通100讲 深度学习框架TensorFlow的应用案例手把手教你ML机器学习算法源码全解析 机器学习算法解析及应用案例数据挖掘算法... 查看详情

elasticsearch全文搜索引擎实战之kibana搭建(代码片段)

1.Kibana介绍Kibana是一个针对Elasticsearch的开源分析及可视化平台,用来搜索、查看交互存储在Elasticsearch索引中的数据。使用Kibana,可以通过各种图表进行高级数据分析及展示。Kibana让海量数据更容易理解。它操作简单,基于浏览... 查看详情

elasticsearch-基础介绍及索引原理分析(代码片段)

介绍Elasticsearch是一个分布式可扩展的实时搜索和分析引擎,一个建立在全文搜索引擎ApacheLucene(TM)基础上的搜索引擎.当然Elasticsearch并不仅仅是Lucene那么简单,它不仅包括了全文搜索功能,还可以进行以下工作:分布式实时文件存储... 查看详情

elasticsearch-基础介绍及索引原理分析(代码片段)

....html介绍Elasticsearch是一个分布式可扩展的实时搜索和分析引擎,一个建立在全文搜索引擎ApacheLucene(TM)基础上的搜索引擎.当然Elasticsearch并不仅仅是Lucene那么简单,它不仅包括了全文搜索功能,还可以进行以下工作:分布式实时文件... 查看详情

rabbitmq一文彻底弄懂rabbitmq的四种交换机原理及springboot实战应用(代码片段)

四大交换机工作原理及实战应用交换机概念direct直连交换机工作模式图解springboot代码Fanout扇出交换机工作模式图解springboot代码Topic主题交换机工作模式图解springboot代码header交换机交换机概念交换机可以理解成具有路由表的路由... 查看详情

opencv4机器学习:决策树原理及分类实战(代码片段)

前言:本专栏主要结合OpenCV4,来实现一些基本的图像处理操作、经典的机器学习算法(比如K-Means、KNN、SVM、决策树、贝叶斯分类器等),以及常用的深度学习算法。系列文章,持续更新:OpenCV4机器学... 查看详情

云服务器ecs实战一文掌握负载均衡服务原理及配置方法(代码片段)

一、负载均衡基本原理概述协议/端口轮询策略会话保持二、云服务器ECS负载均衡相关配置协议&监听配置后端服务器配置健康检查配置测试在上期文章中,介绍了负载均衡的概述及优势,并详细演示了阿里云服务器负... 查看详情

模板引擎thymeleaf介绍及使用(代码片段)

...用2.3常见的模板引擎3.Thymeleaf3.1Thymeleaf介绍3.2Thymeleaf语法规则3.2.1标准表达式语法3.2.2th属性3.3Thymeleaf使用流程1.服务器生成动态页 查看详情

模板引擎thymeleaf介绍及使用(代码片段)

...用2.3常见的模板引擎3.Thymeleaf3.1Thymeleaf介绍3.2Thymeleaf语法规则3.2.1标准表达式语法3.2.2th属性3.3Thymeleaf使用流程1.服务器生成动态页 查看详情

深入浅出spring原理及实战「开发实战系列」spring-cache扩展自定义(注解失效时间+主动刷新缓存)(代码片段)

缓存失效时间支持在方法的注解上指定SpringCache默认是不支持在@Cacheable上添加过期时间的,可以在配置缓存容器时统一指定:@BeanpublicCacheManagercacheManager(@SuppressWarnings("rawtypes")RedisTemplateredisTemplate)Custo 查看详情