Go语言的plugin
Go plugin支持将Go包编译为共享库(.so)的形式单独发布。主程序可以在运行时动态加载这些编译为动态共享库文件的Go插件,从中提取导出(exported)变量或函数的符号并在主程序的包中使用。Go插件的这种特性为Go开发人员提供了更多的灵活性,我们可以使用它来实现支持热插拔的插件系统。
示例
下面例子结构布局:
├── demo
│ ├── go.mod
│ ├── main.go
└── demo-plugin
├── Makefile
├── go.mod
└── plugin.go
在这个示例中,demo 代表主程序工程,demo-plugin 是主程序的插件工程。下面是插件工程的代码:
package main
import "fmt"
func init() {
fmt.Println("plugin: init() called")
}
type myPlugin struct{}
func (p *myPlugin) DoSomething() string {
return "Hello from myPlugin!"
}
var MyPlugin myPlugin
var Int int
func Dump() {
fmt.Printf("plugin: public integer variable Int=%d\n", Int)
}
plugin包和普通的Go包没太多区别,只是plugin包有一个约束:其包名必须为main,我们可以使用以下命令将该plugin编译为共享库(.so)文件:
go build -buildmode=plugin -o plugin.so main.go
下面我们来看一下主程序的代码(main.go):
package main
import (
"fmt"
"log"
"plugin"
)
type Plugin interface {
DoSomething() string
}
func main() {
// 加载插件
p, err := plugin.Open("../demo-plugin/plugin.so")
if err != nil {
fmt.Println("Unable to load plugin:", err)
return
}
// 获取插件实例
symbol, err := p.Lookup("MyPlugin")
if err != nil {
fmt.Println("Unable to find symbol:", err)
return
}
// 转换为插件实现的接口类型
myPlugin, ok := symbol.(Plugin)
if !ok {
fmt.Println("Unexpected type from plugin symbol")
return
}
// 调用插件方法
result := myPlugin.DoSomething()
fmt.Println(result)
// 查找插件导出的Int
pInt, err := p.Lookup("Int")
if err != nil {
fmt.Println("Unable to find symbol:", err)
return
}
*pInt.(*int) = 20
// 查找插件导出的func
dump, err := p.Lookup("Dump")
if err != nil {
log.Fatalln(err)
}
dump.(func())()
}
这个示例中,我们使用 plugin 包提供的 Open 方法在运行时动态加载了插件。然后,我们使用 Lookup 方法查找 MyPlugin 符号,并将其转换为实现了 Plugin 接口的类型。接下来,我们使用插件的方法 DoSomething 进行调用,并打印输出结果。然后,我们再次使用 Lookup 方法查找 Int 变量,并将其值设置为 20。最后,我们使用 Lookup 方法查找插件导出的函数 Dump,并调用它。
编译插件后,运行示例主程序,我们可以看到如下结果:
➜ demo go run main.go
<plugin: init() called
Hello from myPlugin!
plugin: public integer variable Int=20
我们通过plugin包提供的Plugin类型提供的Lookup方法在加载的.so中查找相应的导出符号,比如上面的Int、Dump和MyPlugin等。Lookup方法返回plugin.Symbol类型,而Symbol类型定义如下:
type Symbol any
我们看到Symbol的底层类型是interface{}
,因此它可以承载从plugin中找到的任何类型的变量、函数(得益于函数是一等公民)的符号。而plugin中定义的类型则是不能被主程序查找的,通常主程序也不会依赖plugin中定义的类型。上面主程序定义的Plugin接口类型,它就是一个主程序与plugin之间的约定,plugin中只要暴露实现了该接口的类型实例,主程序便可以通过Plugin接口类型实例与其建立关联并调用plugin中的实现了。
缺点
-
插件目前仅在Linux、FreeBSD和macOS上受支持,因此它们不适合用于便携式应用程序。
-
使用插件的应用程序可能需要仔细配置,以确保程序的各个部分在文件系统(或容器映像)的正确位置可用。相比之下,部署由单个静态可执行文件组成的应用程序是直截了当的。
-
当一些软件包可能要到应用程序开始运行很长时间后才能初始化时,对程序初始化的推理就更加困难了。
-
加载插件的应用程序中的错误可能会被攻击者利用来加载危险或不受信任的库。
-
除非程序的所有部分(应用程序及其所有插件)都使用完全相同的工具链版本、相同的构建标签以及某些标志和环境变量的相同值进行编译,否则可能会发生运行时崩溃。
-
除非应用程序及其插件的所有常见依赖项都是从完全相同的源代码构建的,否则可能会出现类似的崩溃问题。