作者:井卓
文章简介:
逻辑编排提供一站式集成平台,简化了在集成接口、应用和服务时,所涉及的业务逻辑和流程。本文会介绍如何通过ChatGPT学习Golang、以及表达式解释器的实现和Golang解析引擎的基本设计架构。
逻辑编排提供一站式集成平台,简化了在集成接口、应用和服务时,所涉及的业务逻辑和流程。但逻辑编排自研流程中缺少后端的解析引擎,于是我通过ChatGPT高效的学习了Golang,并完成了开发。本文会介绍如何通过ChatGPT对Golang学习教程的提炼和依赖资料的获取路径、表达式解释器的实现和Golang解析引擎的基本设计架构等。
问ChatGPT如何用go跑起来一个"Hello World!" ,尽量精简提问,这样就能更精确的拿到自己想要的内容
通过这个提问可以提炼出两个要点
比起官网上冗长的学习教程,我更想看到 go和我们前端常用的ts有什么区别,这样的内容可以直接问ChatGPT,并且让他给出具体的例子,会得到例子。
变量定义在两种语言的中的区别是不同的,除了语法上的不同。在参数的类型上对于数字类型Golang有 int, float32, float64等多种类型。这是因为Golang作为编译型静态类型语言,在变量定义的时候根据不同的类型所占据的字节数是有差别的,所以对语言的变量类型定义更加严格。但这些数字类型这在TypeScript中都统一称做number。
面向对象编程是可以代码 高内聚,低耦合的方案。可以通过ChatGPT方便的知道语言的特性,在Golang中没有传统的类(class)的概念,也就是常说的oo,它采用了一种不同的面向对象编程模式,称为"结构体(struct)"和"方法(method)",与高级语言中类定义的方式有所区别。
学习go还是为了写后端接口,在了解了基础的语法后,进阶的问如何用go起后端服务,跑起来一个基础的接口用例
在这这个的基础上询问如何实现get请求,post请求,以及如果连接数据库,拿到具体的学习用例,来满足自己的目的。
在逻辑编排中需要配置不同的分支进入的逻辑,需要用Go实现一套规则的解释引擎,也就是表达式解释器。规则引擎的配置交互如图所示 。
需要先了解如何实现一个解释器。解释器(Interpreter)是一个基于用户输入执行源码的工具并拿到结果的工具,解释器会解析源码,生成 AST(Abstract Syntax Tree)抽象语法树,逐个迭代并执行 AST 节点。解释器有三个阶段:词法分析(Lexer),语法分析(Syntax ),执行(Eval)
其中 Lexer 负责将原始文本转换为标记流。解析器将标记流转换为抽象语法树(AST)。 AST 基本上是表示为树数据结构的源代码。访问者通过递归“访问”AST 中的每个节点来进行实际的评估/执行。稍后我们将仔细研究它们中的每一个。
词法分析的过程是解析字符串生成有具体含义的每个单词,这个过程我们需要借助有穷自动机来完成,在此之前我们需要重新了解一下正则表达式,这是所有的步骤的基石。
日常编程大家应该都接触过正达式,用它来匹配字符串等,也可能已经很熟悉其语法了。但我这次想从正则表达式的最基本概念来重新介绍一次,主要想让大家更深地理解它。首先我们要重新定义一下“语言”这个概念。“语言”就是指字符串的集合,其中的字符来自于一个有限的字符集合。也就是说,语言总要定义在一个有限的字符集上,但是语言本身可以既可以是有穷集合,也可以是无穷集合。比如“JavaScripe语言”就是指满足JavaScripe语法的全体字符串的集合,它显然是个无穷集合。当然也可以定义一些简单的语言,比如这个语言{ a }就只有一个成员,那就是一个字母a。后面我们都用大括号{}来表示字符串的集合。所谓正则表达式呢,就是描述一类语言的一种特殊表达式,正则表达式共有2种基本要素:
同时正则表达式定义了3种基本运算规则:
比如我们常见的变量定义改写成正则就是 ^[a-zA-Z_][a-zA-Z0-9_]*$,数字对应的正则是^-?\d+(\.\d+)?$, 我们大家平时熟悉的正则表达式是写成上文这样的字符串形式。但这次我们要自己处理正则表达式,写成字符串显然增加了处理的难度(要解析正则表达式字符串)。所以词法分析库中,需要通过面相对象思想来抽象正则表达式,每一种正则表达式要素或运算编写了一个子类, 具体要如何转换需要借助NFA和DFA来进行。
有穷自动机首先包含一个有限状态的集合,还包含了从一个状态到另外一个状态的转换。有穷自动机看上去就像是一个有向图,其中状态是图的节点,而状态转换则是图的边。此外这些状态中还必须有一个初始状态和至少一个接受状态。下面的图展示了一个有穷自动机,有根从外边来的箭头指向的状态表示初始状态,有个黑圈的状态是接受状态:
有穷自动机怎么处理输入的字符串"ab*a"的流程如下:
3.2.3 非确定性有穷自动机DFA
非确定性有穷自动机(NFA),它允许从一个状态发出多条具有相同符号的边,甚至允许发出标有ε(表示空)符号的边,也就是说,NFA可以不输入任何字符就自动沿ε边转换到下一个状态,他会比NFA接受的状态更多,也是我常用的词法分析方案,也就是说在NFA中从开始状态到结束的状态的路径并不是固定的,甚至有可能是空字符串!
下图展示了一个非确定性有穷自动机:
不同的自动机对应不同的正则表达式,例如正则表达式-?[0-9]+就是对应上述的非确定性有穷自动机,借助这样的有穷自动机我们可以实现把正则拆分到不同的自动机上,来实现词法分析中对字符串的解析, 我们上边的更多正则的规则可以用下图进行描述。
通过NFA我们可以把所有的正则进行解析和完善,并列出相应的对应表关系。把不同的正则抽象成不同的类,用来语法分析,也就是表达式中诸如变量、数字、算数运算符号、逻辑运算符号这样有特定意义的单词,来进入下一步的解析。
词法分析把字符串解析成一个个具有具体意义的单词,一种完整的编程语言,必须在此基础上定义出各种声明、语句和表达式的语法规则。观察我们所熟悉的编程语言,其语法大都有某种递归的性质。例如四则运算与括号的表达式,其每个运算符的两边,都可以是任意的表达式。比如1+a是表达式,(1+a)*(2 – c)也是表达式,((a+b) + c) * (d – e)也是表达式。再比如if语句,其if的块和else的块中还可以再嵌套if语句。我们在词法分析中引入的正则表达式和正则语言无法描述这种结构,如果用DFA来解释,DFA只有有限个状态,它没有办法追溯这种无限递归。所以,编程语言的表达式,并不是正则语言。要引入一种表现能力更强的语言——上下文无关语言。
要介绍上下文无关语言,我们先来了解一下定义上下文无关文法的工具——产生式的写法。我们还是使用编程语言的表达式作为例子,但这次我们假设表达式只有三种——单个表示变量名标识符、括号括起来的表达式和两个表达式相加。比如a是一个变量表达式,a+b是两个变量表达式相加的表达式,(a+b)是一个括号表达式。我们用符号E来表示一个表达式,那么这三种表达式分别可以定义为:
E → E + S | E
E → number | ( S )
这种形式的定义就叫做产生式。出现在→左侧符号E称作非终结符(nonterminal symbol),代表可以继续产生新符号的“文法变量”。 符号→表示非终结符可以“产生”的东西。而上述产生式中的蓝色id、+、(等符号,是具有固定意义的单词,它们不再会产生新的东西,称作终结符(terminal symbol)。注意,非终结符可以出现在产生式的右侧,这就是具有递归性质文法的来源。产生式经过一系列的推导,就能够生成各种完全由终结符组成的句子。比如,我们演示一下表达式(1+2+(3+4))+5的推导过程:
推导树与Ast:
上边我们说到语法分析使用的上下文无关语言,以及描述上下文无关文法的产生式、产生式推导和语法分析树等概念。今天我们就来讨论实际编写语法分析器的方法。今天介绍的这种方法叫做递归下降(recursive descent)法,这是一种适合手写语法编译器的方法,且非常简单。递归下降法对语言所用的文法有一些限制,但递归下降是现阶段主流的语法分析方法,因为它可以由开发人员高度控制,在提供错误信息方面也很有优势,使用递归下降法编写语法分析器无需任何类库,编写简单的分析器时甚至连前面学习的词法分析库都无需使用。我们来看一个例子:现在有一种表示二叉树的字符串表达式,它的文法是:
其中终结符a表示任意一个英文字母,ε表示空。这个文法的含义是,二叉树的节点要么是空,要么是一个字母开头,并带有一对括号,括号中逗号左边是这个节点的左儿子,逗号右边是这个节点的右儿子。例如字符串 A(B(,C(,)),D(,))就表示这样一棵二叉树:
把这样的推导过程用代码表达出来,我们就能构成真正的Ast数,进行下一步的的解释执行。
下边介绍我们用LL文法对一个二元表达式(1 + 2 + (3 + 4)) + 5的解析。
通过语法分析拿到真正的Ast之后,可以做的事情有两种,解释器(compiler) 和 编译器(Interpreter)和,如果有个翻译器把源程序翻译成机器语言,那它就是 编译器。如果一个翻译器可以处理并执行源程序,却不用把它翻译器机器语言,那它就是 解释器。比如我们用的Taro其实本质上就是编译器的一种,解析我们用React成Ast,并翻译成原生的小程序语法。而我们这里的表达式解释器主要是写的是解释器,通过对表达式Ast的解释执行拿到表达式的结果。
下面就是这个计算器的代码。它接受一个表达式,输出一个数字作为结果,正如上一节所示。
这里的 match 语句是一个模式匹配。它的形式是这样:
它根据表达式 exp 的“结构”来进行“分支”操作。每一个分支由两部分组成,左边的是一个“模式”,右边的是一个结果。左边的模式在匹配之后可能会绑定一些变量,它们可以在右边的表达式里面使用。
一般说来,数据的“定义”有多少种情况,用来处理它的“模式”就有多少情况。比如算术表达式有两种情况,数字或者 (op e1 e2)。所以用来处理它的 match 语句就有两种模式。“你所有的情况,我都能处理”,这就是“穷举法”。穷举的思想非常重要,你漏掉的任何一种情况,都非常有可能带来麻烦。所谓的“数学归纳法”,就是这种穷举法在自然数的递归定义上面的表现。因为你穷举了所有的自然数可能被构造的两种形式,所以你能确保定理对“任意自然数”成立。
我们在了解表达式解释器的理论基础上,需要应用于实战,下边是我们一个表达式节点配置的schema,分别包含解释器的上下文以及需要解析的逻辑语句信息。其中 schema 会被翻译成 ( Key1 == 1) && ( Key2 == 2) && ( (Key1 > 1) ||(Key3 >= 2)) 然后放入我们的表达式解释器中拿到真正的执行结果,可以疑问这里的Key是什么,其实这是我们的上下文中存储的变量,我们在最终解释器的模式匹配中,会设置如果遇到变量就去相应的作用域中查找, 由于我们这里实现的表达式解释的上下文的没有嵌套的情况,所以这里的的表达式只有一层。通过以上步骤,我们就实现了表达式解释器,可以当作逻辑编排中的规则引擎进行使用,利用动态表达式来实时修改配置,我们保险产品的显示规则等,下文将继续介绍表达式解释器如何应用到逻辑编排中。
逻辑编排提供一站式集成平台,简化了在集成接口、应用和服务时,所涉及的业务逻辑和流程。提供简便易用的设计方式,快速生成服务接口以及定时任务, 流程设计如下。
在具体交互流程如下,用户可以可视化的配置自己的条件分支,以及不同的分支需要执行的流程结果。
规则解析引擎的的代码实现比较灵活,没有固定的模板。应用设计模式主要是应对代码的复杂性,实际上,解释器模式也不例外。它的代码实现的核心思想,就是将语法解析的工作拆分到为不同的节点类型,以此来避免大而全的解析类。一般的做法是,将语法规则拆分成一些小的独立的单元,然后对每个单元进行解析,最终合并为对整个语法规则的解析。
在架构设计上的逻辑编排引擎的节点类型有开始节点,结束节点, 条件节点, 代码节点, 合并节点,分支节点。所有的节点信息都会被记录在一维的数组中,每个节点的都会有两个字段preId和nextId 记录自己上下节点的ID信息,这是一个类似于双向链表的数据结构。之前我看过内容生产平台的出码部分的源码,数据结构也是类似的。拿到节点数组后,会按照不同的节点type进行分发,从而实例化不同的节点。每个实例节点都继承自相同的父类,需要统一实现 Run从而来处理不同的节点特定的逻辑内容
开始节点
标注逻辑解析引擎开始的节点。
结束节点
作为整个解析引擎的结束,nextId 是空标注程序的结束,需要接口的返回参数, 作为整个接口的输出。
条件节点
条件配置节点就是我们上边实现的表达式解释器,它通过执⾏规则并进⾏推理,能够实现规则匹配、前向推理、后向推理等功能。拿到当前逻辑判断节点的结果 true 或者 false 。决定当前分⽀是否可以继续向下执⾏,这里的表达式解析引擎实现的具体步骤会在下文介绍。
接口调用节点
接口节点可以支持配置请求头,请求链接,请求参数,通过go发送接口,并拿到接口的执行结果,并将结果请求传递到后边的节点。
代码块节点
代码节点的解释执行是通过goja进行的,goja是Golang的一个包用来可以执行js的代码,同时支持定制上下文拦截器 context,来对整个逻辑编排数据的读取进行读取, 通过 getArg 可以读取到上文中传入当前节点的参数,setReturn 可以设置代码中需要返回的数据。
分支节点
记录逻辑编排中会进行分支的地方,记录叉的节点信息,并发的判断不同分叉节点的逻辑是否通过,从而判断具体是哪个分支才能向后执行。
合并节点
记录逻辑编排中合并节点信息的地方,用于向下传递参数信息。
任何一种编程中,作用域是程序中定义的变量所存在的区域,超过该区域变量就不能被访问。所以每个节点支持配置自己的输出和输入的参数,用来暴露出给全局的参数信息,前边节点的输出,可以当作后边节点的输入。
每节点都会有 inputArgs 参数,用来存储输入的参数信息。输入的参数类型分为以下几种globalData 全局参数 , flowData 节点参数, immediate 立即数,具体结构如下。其中 immediate可以直接的读取数据,globalData 和 flowData会被从作用域从进行存取。
每节点都会有 outputArgs 参数,用来在作用域中记录,当前节点暴露出的字段信息有哪些,用于向下边节点传递信息。
通过本文的阅读,会了解如何通过ChatGPT对教程的提炼和依赖资料的获取,以及Golang解析引擎的基本设计架构: