Hello folks,我是 Luga,今天我们来聊一下云原生网关 Traefik 堆栈插件及脚本运行载体 ——Yaegi 解释器。
— 01—
什么是编译器?
编程语言有很多种,每种语言都有自己的语法和规则。这些语言被设计成类似于英语一样易于理解和编写。但是,计算机不能直接理解这些语言,它们只能理解用二进制代码表示的指令,即 0 和 1。
因此,为了让计算机执行代码,需要将高级语言源代码转换为机器级语言程序。这个过程需要使用编译器或解释器。通常,程序员使用高级编程语言编写程序,然后使用编译器将其转换为可执行代码,或使用解释器直接解释执行。
编译器和解释器都是将高级语言源代码转换为机器级语言程序的工具,但是它们的工作方式有所不同。
那么,什么是编译器?
通常,从本质上而言,编译器是一种翻译器,将高级编程语言作为输入,生成低级语言(如汇编语言或机器语言)的输出。它本质上是一个计算机程序,用于将用编程语言编写的代码或指令转换为机器代码,即由0和1组成的二进制语言,这是计算机处理器可以理解的语言。
编译器的工作过程通常分为三个阶段:词法分析、语法分析和代码生成。具体如下所示:
1、词法分析(Lexing):Go 编译器首先将源代码分解成一个个标记(Token),这个过程称为词法分析。词法分析器扫描源代码,将代码分解成一个个标记,每个标记代表一个关键字、标识符、常量或运算符等。
2、语法分析(Parsing):在词法分析之后,Go 编译器会将标记序列转换为抽象语法树(AST)。抽象语法树是一种表示代码结构的数据结构,它可以更方便地进行代码分析和变换。
3、代码生成(Code Generation):在语法分析之后,Go 编译器会进行类型检查。类型检查器会检查变量、表达式和函数的类型是否一致,以及是否符合语言规范。在类型检查之通过后,Go 编译器会将抽象语法树转换为目标机器代码。具体的代码生成过程会依赖于操作系统和编译器架构等因素。
具体工作流程,可参考如下示意图所示:
需要注意的是,机器代码/可执行文件形式的输出不是 100% 通用的,它包括特定于处理器的指令。例如,AMD 可能无法理解为英特尔处理器生成的二进制/机器代码。因此,编译器也需要特定于平台。
— 02—
什么是解释器?
相对于编译器而言,解释器是一种将编程语言翻译成可理解语言的程序。它是一种用于将高级程序语句转换为机器代码的计算机程序。包括预编译代码、源代码等。
其实,若我们从另外一种角度窥探,便会发现:解释器的工作方式或多或少与编译器类似。它们功能之间的主要区别在于解释器不生成任何中间代码形式,而是逐行读取程序检查错误,并同时运行程序。
同样,如果我们看一下解释器的历史定义,解释器是逐行读取源代码并在运行时生成机器指令的软件。因此,它不会预编译任何内容,而是即时解释提供的输入,以指示 CPU 按顺序执行任务。
下图说明了解释器如何工作的简单流程,具体如下所示:
与编译器一样,解释器也不是通用的,其设计目的是读取特定的输入格式。例如,解释器可以被设计为解释 JavaScript 源代码或 Java 字节码或任何其他输入格式。
另请注意,不同的编程语言以不同的方式实现解释器,我们将在后续的文章中进行介绍。
— 03—
基于 Golang 的解释器类型
Go 语言本身是一种编译型语言,但是也可以通过编写解释器的方式来实现解释执行。以下列举几个基于 Go 语言的解释器:
1、Yaegi 是一个使用 Go 语言编写的解释器,它可以解析和执行 Go 代码。相比于编译器,Yaegi 是一个更为灵活和交互式的工具,可以在运行时动态解析和执行 Go 代码。它不需要提前编译源代码,而是直接从字符串或文件读取代码并解释执行。
2、Goja 是一个使用 Go 语言编写的 JavaScript 解释器,基于 ECMAScript 5.1 标准,并支持完整的 ECMAScript 5.1 语言特性。相比于传统的 JavaScript 解释器,Goja 具有更好的性能和更高的可扩展性,可以在 Go 应用程序中嵌入 JavaScript 脚本。
Goja 提供了完整的 ECMAScript 5.1 语言特性支持,包括变量声明、数据类型、函数定义、对象和数组等。它还支持 JavaScript 中的闭包、原型继承、异常处理等高级特性,可以满足大多数 JavaScript 应用程序的需求。除此之外,Goja 还支持在解释器中添加新的对象和函数,使得开发人员可以根据实际需求扩展和定制解释器。
3、Gisp:Gisp 是一个使用 Go 语言编写的 Lisp 解释器,它支持基本的 Lisp 语法和函数,并提供了一些扩展功能,如 Go 函数和类型的调用。
Gisp 是一个轻量级的 Lisp 方言,它的语法和特性与 Scheme 和 Clojure 有很多相似之处。它支持动态类型、高阶函数、闭包、宏扩展等 Lisp 特性,并能够直接调用 Go 语言的函数和方法。Gisp 的目标是提供一个简单、易于学习和使用的 Lisp 语言,同时具有 Go 语言的可靠性和性能。
4、GopherLua 是一个基于 Go 语言实现的 Lua 解释器,支持 Lua 5.1 语言规范。相比于传统的 Lua 解释器,GopherLua 具有更好的性能和更高的可扩展性,可以在 Go 应用程序中嵌入 Lua 脚本,实现动态配置和扩展。
GopherLua 的特点之一是轻量级设计,核心代码库非常小,同时还提供了可插拔的扩展机制,可以根据实际需求添加和删除功能。此外,GopherLua 还支持 Lua 中的协程和元表,使得使用 Lua 进行并发编程和元编程变得更加方便和灵活。
除此之外,GopherLua 还提供了与 Go 语言的无缝集成,可以直接调用 Go 语言的函数和方法,并且可以在 Go 应用程序中嵌入 Lua 脚本。这使得开发人员可以利用 Go 语言的强大功能来扩展和优化 Lua 脚本,同时也可以在 Go 应用程序中使用 Lua 脚本来实现动态配置和扩展。
5、GoRuby 是一个基于 Go 语言实现的 Ruby 解释器,支持大多数的 Ruby 语言特性,并可以在 Go 应用程序中嵌入 Ruby 脚本。相比于传统的 Ruby 解释器,GoRuby 具有更好的性能和更高的可扩展性,可以在 Go 应用程序中使用 Ruby 脚本来实现动态配置和扩展。
GoRuby 的实现基于 Ruby MRI(Matz's Ruby Interpreter),支持 Ruby 2.6 语言规范,并提供了与 MRI 类似的 API 和命令行界面。除此之外,GoRuby 还支持 Ruby on Rails 框架,可以在 Go 应用程序中嵌入 Rails 应用程序,并与其他 Go 组件集成。
6、Golo:Golo 是一个基于 Go 语言实现的 Lisp 解释器。它支持大多数的 Lisp 特性,包括宏扩展和动态类型。
与上述相比较,Golo 是一个更加复杂和功能更为丰富的 Lisp 方言,其语法和特性与 Common Lisp 有很多相似之处。它支持静态类型、多重继承、泛型、宏扩展等特性,并且具有内置的模块系统和 REPL(交互式解释器)。Golo 的目标是提供一个强大、灵活和可扩展的 Lisp 语言,与 Go 语言的可靠性和性能相结合。
虽然 Gisp 和 Golo 都是基于 Go 语言实现的 Lisp 方言,但是它们的目标和设计哲学略有不同,适用于不同的应用场景。Gisp 适合于快速原型开发和小型项目,而 Golo 则适合于大型项目和需要高度灵活性和可扩展性的应用程序。
— 03—
什么是 Yaegi 解释器以及为什么 Golang 或 Traefik 需要?
作为 Go 的核心解释器,Yaegi 是由 Containous( Traefik和TraefikEE背后的公司)开发的开源项目,旨在在 Go 运行时之上引入可执行的 Go 脚本、嵌入式插件、交互式 Shell 和即时原型。
虽然 Go 是一种静态和强类型语言,但它的感觉却像一种动态语言。标准库甚至提供了编译器使用的 Go 解析器和反射系统,以便与运行时动态交互。那么为什么不采取最后一个逻辑步骤并最终构建一个完整的 Go 解释器呢?
通常用于高级脚本编写和低级实现的编程语言是不同的,但通过 Go,我们有机会将两者统一起来。想象一下,Python 的所有 C/C++/Java 快速库都用 Python 编写。这正是 Yaegi 对于 Go 的价值所在,或者反过来。它消除了语法切换的负担,无需重写或修改慢速代码以提高性能,并且能够在脚本级别完全访问 goroutine、通道、类型安全等。
基于 Golang 角度而言, 虽然 Go 语言本身是一种编译型语言,但是有时候需要在运行时动态执行代码。Yaegi 支持交互式探索和调试,可以帮助开发人员快速测试和验证代码。它还提供了内置的标准库和支持调用外部 Go 包的功能,使得使用 Yaegi 编写和执行 Go 代码变得更加方便和灵活。
除了作为解释器之外,Yaegi 还可以作为 Go 语言的脚本引擎使用。它可以在应用程序中嵌入 Go 脚本,实现动态配置和扩展。此外,Yaegi 还支持在沙箱环境中运行代码,避免了潜在的安全风险。
基于 Traefik 角度而言,作为一种基于 Go 语言编写的流行的开源反向代理和负载均衡器,Traefik 需要支持动态配置和路由规则,这就需要在运行时动态解析和执行配置文件。为了实现这个功能,Traefik 使用了 Yaegi 解释器,使得 Traefik 可以支持用户自定义的配置文件,并且可以在运行时动态加载和执行。
— 04—
Yaegi 解释器实现原理及案例
基于上述所述,Yaegi是一个使用 Go 语言编写的解释器,可以解析和执行 Go 代码。在 Yaegi 的设计实现中,主要包含以下几个方面的内容,仅供参考:
1. 词法分析器:Yaegi 首先需要将输入的 Go 代码转化为词法单元,这个过程称为词法分析。词法分析器会将输入的 Go 代码分解为各种不同类型的词法单元,例如关键字、标识符、字面量和运算符等。
2. 语法分析器:Yaegi 将词法单元转化为语法树,这个过程称为语法分析。语法分析器会根据词法单元之间的语法规则,将其组织成一棵语法树。这棵语法树可以用来表示输入的 Go 代码的结构,包括函数、变量、语句和表达式等。
3. 解释器:Yaegi 的核心功能是解释器,它会遍历语法树,并执行其中的每个语句和表达式。解释器会根据语法树中的节点类型,执行相应的操作,例如对变量赋值、调用函数或执行运算等。在执行过程中,解释器还会维护一个运行时环境,包括变量和函数的作用域、调用栈和堆栈等。
4. 标准库和外部包:为了支持 Go 代码的执行,Yaegi 还提供了内置的标准库和支持调用外部 Go 包的功能。标准库包括各种常用的函数和数据类型,例如 fmt、strings 和 time 等。外部包可以通过 import 语句引入,并可以在 Yaegi 中直接调用其中的函数和方法。
这里,我们以 Yaegi 在运行时加载并执行 Go 包为简要场景,即 Yaegi 可以用于在运行时加载并执行整个 Go 包,而不需要将包编译成可执行的二进制文件。这对于创建可扩展的应用程序或插件非常有用,这些应用程序或插件可以加载和使用新功能,而无需完全重新编译。具体代码如下所示:
package main import ( "fmt" "github.com/traefik/yaegi/interp" "os" "plugin" ) func main() { // Create a new interpreter instance i := interp.New(interp.Options{}) // Load a Go plugin pluginPath := "./myplugin.so" _, err := i.Eval(fmt.Sprintf(`plugin, err := plugin.Open("%s")`, pluginPath)) if err != nil { fmt.Println(err) return } // Extract a function from the plugin and call it funcName := "MyPluginFunc" pluginFunc, err := i.Eval(fmt.Sprintf(`plugin.Lookup("%s")`, funcName)) if err != nil { fmt.Println(err) return } res := pluginFunc.(func(string) string)("Hello, Traefik!") fmt.Println(res) }
在此示例中,我们使用 Yaegi 在运行时使用标准库中的 “plugin” 包加载 Go 插件。 我们使用 Yaegi 从插件中提取名为“MyPluginFunc”的函数,然后使用参数“Hello, Traefik!”调用该函数。 函数调用的结果被打印到控制台。
总之,Yaegi 是一个功能强大、灵活和交互式的 Go 语言解释器,可以帮助开发人员快速测试和验证代码,同时具有内置的标准库和支持调用外部 Go 包的功能。它还可以作为 Go 语言的脚本引擎使用,实现动态配置和扩展,同时可以在沙箱环境中运行代码,保证了应用程序的安全性。
Adiós !