首页 > 其他分享 >Golang 规则引擎原理及实战

Golang 规则引擎原理及实战

时间:2023-07-24 14:55:53浏览次数:36  
标签:实战 return ast expr parser Golang 引擎 规则

本文主要介绍规则引擎在 golang 中的使用,将首先介绍 golang 中主要的规则引擎框架,然后利用 golang 原生的 parser 搭建一个简单的规则引擎实现基本的 bool 表达式解析工作。

背景

随着业务代码的不断迭代,诞生出了越来越多的 if-else,并且 if-else 中的逻辑越来越复杂,导致代码逻辑复杂、维护性差、可读性差、修改风险高等缺陷。

复杂的 if-else 逻辑其实对应的是一条条的规则,满足对应的规则在执行对应的操作,即 if-else 中的条件就是一个对应的 bool 表达式:

  1.   |--bool 表达式--|
  2.   if a == 1 && b == 2 {
  3.   // do some business
  4.   }

一个复杂的逻辑表示一条对应的规则,将这些规则用 bool 表达式表示之后,便可以按照对应的规则执行操作,大大减少 if-else 的应用:

  1.   if 规则 {
  2.   // do some business
  3.   }

而如何解析这些 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 的资源,在请求量较大的系统当中就有可能成为系统的性能屏障。

  1.   if type == 1 && product_id = 3{
  2.   //...
  3.   }

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

基于Go parser库打造规则引擎

parser 库介绍

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

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

  1.   // 使用go语法表示的bool表达式,in_array为函数调用
  2.   expr := `product_id == "3" && order_type == "0" && in_array(capacity_level, []string{"900","1100"}) && carpool_type == "0"`
  3.    
  4.   // 使用go parser解析上述表达式,返回结果为一颗ast
  5.   parseResult, err := parser.ParseExpr(expr)
  6.   if err != nil {
  7.   fmt.Println(err)
  8.   return
  9.   }
  10.    
  11.   // 打印该ast
  12.   ast.Print(nil, parseResult)

可以得到如下的结果(一颗二叉树):

  1.   0 *ast.BinaryExpr {
  2.   1 . X: *ast.BinaryExpr {
  3.   2 . . X: *ast.BinaryExpr {
  4.   3 . . . X: *ast.BinaryExpr {
  5.   4 . . . . X: *ast.Ident {
  6.   5 . . . . . NamePos: 1
  7.   6 . . . . . Name: "product_id"
  8.   7 . . . . }
  9.   8 . . . . OpPos: 12
  10.   9 . . . . Op: ==
  11.   10 . . . . Y: *ast.BasicLit {
  12.   11 . . . . . ValuePos: 15
  13.   12 . . . . . Kind: STRING
  14.   13 . . . . . Value: "\"3\""
  15.   14 . . . . }
  16.   15 . . . }
  17.   16 . . . OpPos: 19
  18.   17 . . . Op: &&
  19.   18 . . . Y: *ast.BinaryExpr {
  20.   19 . . . . X: *ast.Ident {
  21.   20 . . . . . NamePos: 22
  22.   21 . . . . . Name: "order_type"
  23.   22 . . . . }
  24.   23 . . . . OpPos: 33
  25.   24 . . . . Op: ==
  26.   25 . . . . Y: *ast.BasicLit {
  27.   26 . . . . . ValuePos: 36
  28.   27 . . . . . Kind: STRING
  29.   28 . . . . . Value: "\"0\""
  30.   29 . . . . }
  31.   30 . . . }
  32.   31 . . }
  33.   32 . . OpPos: 40
  34.   33 . . Op: &&
  35.   34 . . Y: *ast.CallExpr {
  36.   35 . . . Fun: *ast.Ident {
  37.   36 . . . . NamePos: 43
  38.   37 . . . . Name: "in_array"
  39.   38 . . . }
  40.   39 . . . Lparen: 51
  41.   40 . . . Args: []ast.Expr (len = 2) {
  42.   41 . . . . 0: *ast.Ident {
  43.   42 . . . . . NamePos: 52
  44.   43 . . . . . Name: "capacity_level"
  45.   44 . . . . }
  46.   45 . . . . 1: *ast.CompositeLit {
  47.   46 . . . . . Type: *ast.ArrayType {
  48.   47 . . . . . . Lbrack: 68
  49.   48 . . . . . . Elt: *ast.Ident {
  50.   49 . . . . . . . NamePos: 70
  51.   50 . . . . . . . Name: "string"
  52.   51 . . . . . . }
  53.   52 . . . . . }
  54.   53 . . . . . Lbrace: 76
  55.   54 . . . . . Elts: []ast.Expr (len = 2) {
  56.   55 . . . . . . 0: *ast.BasicLit {
  57.   56 . . . . . . . ValuePos: 77
  58.   57 . . . . . . . Kind: STRING
  59.   58 . . . . . . . Value: "\"900\""
  60.   59 . . . . . . }
  61.   60 . . . . . . 1: *ast.BasicLit {
  62.   61 . . . . . . . ValuePos: 83
  63.   62 . . . . . . . Kind: STRING
  64.   63 . . . . . . . Value: "\"1100\""
  65.   64 . . . . . . }
  66.   65 . . . . . }
  67.   66 . . . . . Rbrace: 89
  68.   67 . . . . . Incomplete: false
  69.   68 . . . . }
  70.   69 . . . }
  71.   70 . . . Ellipsis: 0
  72.   71 . . . Rparen: 90
  73.   72 . . }
  74.   73 . }
  75.   74 . OpPos: 92
  76.   75 . Op: &&
  77.   76 . Y: *ast.BinaryExpr {
  78.   77 . . X: *ast.Ident {
  79.   78 . . . NamePos: 95
  80.   79 . . . Name: "carpool_type"
  81.   80 . . }
  82.   81 . . OpPos: 108
  83.   82 . . Op: ==
  84.   83 . . Y: *ast.BasicLit {
  85.   84 . . . ValuePos: 111
  86.   85 . . . Kind: STRING
  87.   86 . . . Value: "\"0\""
  88.   87 . . }
  89.   88 . }
  90.   89 }

打造基于parser库的规则引擎

将 parser 解析出来的这颗二叉树画出来:

可以看到,有了 Golang 原生的语法解析器,我们只需要后序遍历这棵二叉树,然后实现一套 AST 与对应数据map的映射关系即可实现一个简单的规则引擎。

其中,AST 与对应数据map的映射关系的实现代码的主要结构如下:

  1.   func eval(expr ast.Expr, data map[string]interface{}) interface{} {
  2.   switch expr := expr.(type) {
  3.   case *ast.BasicLit: // 匹配到数据
  4.   return getlitValue(expr)
  5.   case *ast.BinaryExpr: // 匹配到子树
  6.   // 后序遍历
  7.   x := eval(expr.X, data) // 左子树结果
  8.   y := eval(expr.Y, data) // 右子树结果
  9.   if x == nil || y == nil {
  10.   return errors.New(fmt.Sprintf("%+v, %+v is nil", x, y))
  11.   }
  12.   op := expr.Op // 运算符
  13.    
  14.   // 按照不同类型执行运算
  15.   switch x.(type) {
  16.   case int64:
  17.   return calculateForInt(x, y, op)
  18.   case bool:
  19.   return calculateForBool(x, y, op)
  20.   case string:
  21.   return calculateForString(x, y, op)
  22.   case error:
  23.   return errors.New(fmt.Sprintf("%+v %+v %+v eval failed", x, op, y))
  24.   default:
  25.   return errors.New(fmt.Sprintf("%+v op is not support", op))
  26.   }
  27.   case *ast.CallExpr: // 匹配到函数
  28.   return calculateForFunc(expr.Fun.(*ast.Ident).Name, expr.Args, data)
  29.   case *ast.ParenExpr: // 匹配到括号
  30.   return eval(expr.X, data)
  31.   case *ast.Ident: // 匹配到变量
  32.   return data[expr.Name]
  33.   default:
  34.   return errors.New(fmt.Sprintf("%x type is not support", expr))
  35.   }
  36.   }

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

性能对比

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

  1.   BenchmarkGoParser_Match-8 127189 8912 ns/op // 基于 go parser 实现的规则引擎
  2.   BenchmarkGval_Match-8 63584 18358 ns/op // gval
  3.   BenchmarkGovaluateParser_Match-8 13628 86955 ns/op // govaluate
  4.   BenchmarkYqlParser_Match-8 10364 112481 ns/op // yql

总结

可以看到在使用原生的 parser 实现的规则引擎在性能上具有较大的优势,但缺点在于需要自己实现一套 AST 与对应数据map的映射关系,并且受限于 go 原生 parser 库的限制导致规则的定义语言比较繁琐,这些也都是为什么会有其他规则引擎框架诞生的原因,但不可否认基于原生 parser 库打造的规则引擎的性能还是足够优秀的,因此在一些比较简单的规则匹配场景中还是优先考虑使用原生 parser,可以最大效率的实现降本增效的效果。

标签:实战,return,ast,expr,parser,Golang,引擎,规则
From: https://www.cnblogs.com/ExMan/p/17577233.html

相关文章

  • 【项目实战】Kafka 重平衡 Consumer Group Rebalance 机制
    ......
  • 济南 S NOIP 刷题实战梳理营游记
    前言期末砸力。这次暑假去两个营,一个在烟台,一个在青岛。在烟台的都是学算法,扔到目录里了,这篇文章就是来讲济南营的。一共十二天,每天上午八点到十二点打比赛,然后吃饭,然后讲题。Day-1\(6h\)的大巴,绷不住了,中途在潍坊西休息,热死了。到了济南,住在酒店旁边,楼下全是吃的,很赞。......
  • 想在golang里用好泛型还挺难的
    golang的泛型已经出来了一年多了,从提案被接受开始我就在关注泛型了,如今不管是在生产环境还是开源项目里我都写了不少泛型代码,是时候全面得回顾下golang泛型的使用体验了。先说说结论,好用是好用,但问题也很多,有些问题比较影响使用体验,到了不吐不快的地步了。这篇文章不会教你泛型......
  • 1.负载均衡服务LVS及三种模型实战案例
    知识小课堂1.负载均衡会话保持sessionsticky:同一用户调度固定服务器sessionreplication:每台服务器拥有全部sessionsessionserver:专门的session服务器2.LVS集群工作模式NAT:DR:(必须在同一网络,用改内核参数)TUNNEL:(可以跨网络,不用改内核参数,需要单独增加tunnel网卡)FUL......
  • 火山引擎DataLeap如何解决SLA治理难题(三): 平台架构与未来展望
    更多技术交流、求职机会,欢迎关注字节跳动数据平台微信公众号,回复【1】进入官方交流群平台架构总结火山引擎DataLeapSLA平台整体主要分为基础组件、规划式治理服务、响应式治理服务三大块,系统组件架构图如下:规划式治理服务所谓“规划式治理”,即在问题发现前治理,通过主动规划约定SL......
  • 火山引擎DataLeap如何解决SLA治理难题(三): 平台架构与未来展望
    更多技术交流、求职机会,欢迎关注字节跳动数据平台微信公众号,回复【1】进入官方交流群平台架构总结火山引擎DataLeapSLA平台整体主要分为基础组件、规划式治理服务、响应式治理服务三大块,系统组件架构图如下: 规划式治理服务所谓“规划式治理”,即在问题发现前治理,......
  • AI识别检验报告 -PaddleNLP UIE-X 在医疗领域的实战
    目录UIE-X在医疗领域的实战1.项目背景2.案例简介3.环境准备数据转换5.模型微调6.模型评估7.Taskflow一键部署UIE-X在医疗领域的实战PaddleNLP全新发布UIE-X......
  • Java 诊断工具 Arthas 常见命令(超详细实战教程)
    基本概念  云原生这么多微服务,当然需要一个诊断利器来排查问题。  Arthas是阿里开源的Java诊断工具,深受开发者喜爱。在线排查问题,无需重启;动态跟踪Java代码;实时监控JVM状态。Arthas支持JDK6+,支持Linux/Mac/Windows,采用命令行交互模式,同时提供丰富的Tab......
  • 打造原生 WebGL 2D 引擎:一场创意与技术的融合
    打造原生WebGL2D引擎:一场创意与技术的融合1.引言在当今数字化时代,网页的功能越来越丰富,已经远远超越了传统的文本和图片呈现。我们生活在一个充满交互性和视觉魅力的网络世界。每天都会遇到许多令人惊叹的网页效果和动画。作为一个Web3D图形的开发,希望可以通过网页来实现更多......
  • 成都站丨阿里云 Serverless 技术实战营邀你来玩!
    活动简介“Serverless技术实战与创新沙龙”是一场以Serverless为主题的开发者活动,活动受众以关注Serverless技术的开发者、企业决策人、云原生领域创业者为主,活动形式为演讲、动手实操,让开发者通过一个下午的时间增进对Serverless技术的理解,快速上手Serverless,拥抱云......