一、技术背景
1.1 程序的动态链接技术
在实际开发过程中,我们经常需要动态地更新程序的功能,或者在不变更程序主体文件的情况下添加或者更新程序模块。
1.1.1 动态链接库
首先最常见的是windows平台所支持的动态链接库(Dynamic Link Library),一般后缀名为.dll
。其优势非常明显:
- 多个程序可以共享代码和数据。即多个程序加载同一个DLL文件。
- 可以自然地将程序划分为若干个模块。每个模块输出为单独的DLL文件,由主程序加载执行。
- 跨语言调用。由于DLL文件是语言无关的,一个DLL文件可以被多种编程语言加载执行。
- 便于更新。在程序更新过程中,仅更新对应模块的DLL文件即可,无需重新部署整个程序。
- 为热更新提供技术可能性。动态链接库可以通过编程手段实现加载和卸载,以此可以支持不重启程序的情况下更新模块。
- 为程序提供编程接口。可以将自己程序的调用接口封装为DLL文件,供其他程序调用。
1.1.2 动态共享对象
在Linux平台,此项技术名为动态共享对象(dynamic shared objects),常见后缀名为.so
。
动态共享对象除了上述“动态链接库”的优势之外,也能解决由于Linux的开放性带来的底层接口兼容问题。即通过动态共享对象封装操作系统底层接口,对外提供统一的调用接口,以供上层应用程序调用。相当于提供了一层兼容层。
1.1.3 非编译语言的动态技术
非编译语言,由于本身是通过源代码发布,所以实现动态加载程序模块或者更新模块,直接修改源代码即可。思路简单且容易实现。
1.2 Golang 的动态技术
Golang作为编译型的开发语言,本身并不支持通过源代码实现动态加载和更新。但Golang官方提供了Plugin技术,实现动态加载。
通过在编译时添加参数,将Go程序编译为 Plugin:
go build -buildmode=plugin
但是此技术在当前版本(1.19)局限性非常大。通过其文档 https://pkg.go.dev/plugin 可知:
- 平台限制,目前仅支持:Linux, FreeBSD 和 macOS
- 卸载限制,仅支持动态加载,不支持动态卸载。
- 不提供统一接口,只能通过反射处理Plugin内部的属性和函数。
并且上述问题,Golang官方并不打算解决……
二、Golang 的第三方解释器(Yaegi)
解释器一般只存在于脚本语言中,但是Traefik为了实现动态加载的插件功能,开发了一个Golang的解释器。提供了在运行时直接执行Golang源代码的能力。
参考项目:https://github.com/traefik/yaegi
2.1 使用场景
yaegi 项目官方推荐三种场景:
- 内嵌解释器
- 动态扩展框架
- 命令行解释器
并且官方针对上述三种场景,均给出了相应的示例:
2.1.1 内嵌解释器
package main
import (
"github.com/traefik/yaegi/interp"
"github.com/traefik/yaegi/stdlib"
)
func main() {
i := interp.New(interp.Options{})
i.Use(stdlib.Symbols)
_, err := i.eva l(`import "fmt"`)
if err != nil {
panic(err)
}
_, err = i.eva l(`fmt.Println("Hello Yaegi")`)
if err != nil {
panic(err)
}
}
2.1.2 动态扩展框架
package main
import "github.com/traefik/yaegi/interp"
const src = `package foo
func Bar(s string) string { return s + "-Foo" }`
func main() {
i := interp.New(interp.Options{})
_, err := i.eva l(src)
if err != nil {
panic(err)
}
v, err := i.eva l("foo.Bar")
if err != nil {
panic(err)
}
bar := v.Interface().(func(string) string)
r := bar("Kung")
println(r)
}
2.1.3 命令行解释器
Yaegi提供了一个命令行工具,实现了 读取-执行-显示 的循环。
$ yaegi
> 1 + 2
3
> import "fmt"
> fmt.Println("Hello World")
Hello World
>
2.2 数据交互
数据交互方式比较多,需要注意的是从解释器内部返回的数据都是 reflect.Value
类型,获取其实际的值需要类型转换。
2.2.1 数据输入
可以有(但不限于)下述四种方法:
- 通过 os.Args 传入数据
- 通过 环境变量 传入数据
- 通过 赋值语句 传入数据
- 通过 函数调用 传入数据
下面是我自己写的代码示例:
package main
import (
"fmt"
"github.com/traefik/yaegi/interp"
"github.com/traefik/yaegi/stdlib"
)
func main() {
{ // 通过 os.Args 传入数据
i := interp.New(interp.Options{
Args: []string{"666"},
})
i.Use(stdlib.Symbols)
i.eva l(`import "fmt"`)
i.eva l(`import "os"`)
i.eva l(`fmt.Printf("os.Args[0] --- %s\n", os.Args[0])`)
// os.Args[0] --- 666
}
{ // 通过 环境变量 传入数据
i := interp.New(interp.Options{
Env: []string{"inputEnv=666"},
})
i.Use(stdlib.Symbols)
i.eva l(`import "fmt"`)
i.eva l(`import "os"`)
i.eva l(`fmt.Printf("os.Getenv(\"inputEnv\") --- %s\n", os.Getenv("inputEnv"))`)
// os.Getenv("inputEnv") --- 666
}
{ // 执行赋值语句传入数据
i := interp.New(interp.Options{})
i.Use(stdlib.S