概述
最近 yiran 推荐了一个 Go 语言的 plugin 机制,虽然这个东西在 1.8 版本就有了,但是出于孤陋寡闻一直都不知道,然后就搜索了一下,发现居然就是以前在 C/C++ 中的动态链接库,马上就懂了,这里就展示一下如何在 Go 中创建并使用 plugin(动态链接库)。
这里我参考的是 Github 上的一个开源项目 vladimirvivien/go-plugin-example,这个示例演示的是如何通过动态加载不同的链接库,从而实现在不修改代码的情况下,使用不同版本的代码,这个特性在热更新中特别有用。
并且我做了一些额外的测试,下面会介绍我做过的这些测试,完整代码你可以在我的 Blog Codes Repo 中找到。
使用方式
Go plugin 的用法相对也比较简单,可以这么拆分为 3 步:
1. 加载动态库
[root@liqiang.io]# cat main.go
import "plugin"
mod := "./chinese/greet.so"
plug, err := plugin.Open(mod)
if err != nil {
panic(err)
}
直接通过 import "plugin"
,然后使用 plugin.Open
即可加载动态库了。
2. 查找目标函数(变量)
symGreeter, err := plug.Lookup("Greeter")
if err != nil {
panic(err)
}
这里可以通过在加载进来的动态库中寻找目标函数,但是加载进来的目标函数的类型是:Symbol
类型,所以得转换一下。
3. 转换目标函数
var greeter Greeter
greeter, ok := symGreeter.(Greeter)
if !ok {
fmt.Println("unexpected type from module symbol")
os.Exit(1)
}
然后就可以直接调用了:greeter.Greet()
,整个结构就是这么简单。
一些尝试
运行过程中(已加载完毕),动态库被删除
那么如果运行过程中,动态库被删除了会怎样?所以我决定试验一下:
[root@liqiang.io shell1]# go run greeter.go -lang chinese
你好宇宙
你好宇宙
你好宇宙 <---- chi.so 已被删除,不影响代码运行
你好宇宙
你好宇宙
运行过程中动态更换
为了验证动态替换的效果,我修改了一下原来的代码,加了一个 Http Server(你也可以用 signal 代替),然后动态修改语言,例如:
greeter := lookupGreeter(mod)
http.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) {
mod = "./eng/eng.so"
greeter = lookupGreeter(mod)
writer.Write([]byte("ok"))
})
go func() {
http.ListenAndServe(":8080", nil)
}()
for {
greeter.Greet()
time.Sleep(time.Second)
}
然后我以 chinese 为参数启动,在运行过程中修改语言:
go run greeter.go -lang chinese
你好宇宙
你好宇宙
你好宇宙
Hello Universe <---- 修改语言,成功修改
Hello Universe
Hello Universe
然后可以看到,效果是成功的!
运行过程中,plugin 的函数 panic
作为一个 Plugin,我们最担心的就是 plugin 中的程序异常了,会不会导致我们的主程序异常,所以我尝试在 Plugin 的代码中使用了 Panic,然后再主程序中进行捕获这个 panic:
Plugin 的代码:
[root@liqiang.io]# cat panic/greet.go
func (g greeting) Greet() {
panic("I'm gone.")
}
主程序代码:
[root@liqiang.io]# cat main.go
for {
func() {
defer func() {
if err := recover(); err != nil {
fmt.Printf("recovered from panic: %v\n", err)
}
}()
greeter.Greet()
}()
time.Sleep(time.Second)
}
然后在运行的时候,我分别替换为 panic 版本和中文版本的插件:
- 运行代码,等待几秒
- 替换成 panic 版本:
curl http://localhost:8080\?mod\=panic
- 替换成中文版本:
curl http://localhost:8080\?mod\=chinese
然后可以看到输出是这样的:
[root@liqiang.io]# go run ./main.go
Hello Universe
Hello Universe
Hello Universe
recovered from panic: I'm gone.
recovered from panic: I'm gone.
recovered from panic: I'm gone.
你好宇宙
你好宇宙
结论就是可以容许 Panic。
运行过程中 Plugin 调用 os.Exit
既然 panic 是可以容许的,那么如果 Plugin 调用了 os.Exit(1)
会怎么样?会不会导致主程序也被迫退出了,于是我写了一个新的 Plugin 代码:
[root@liqiang.io]# cat exit/greet.go
package main
import "os"
type greeting string
func (g greeting) Greet() {
os.Exit(1)
}
然后执行代码,再替换成这个 exit
的版本,然后发现,主程序退出了!这很危险,如果 Plugin 的代码写的不好,那很容易就危及到了主程序的正常运行,所以这个还是需要担心一下的,那么有没有方法避免呢?我第一个想法就是替换 os.Exit
的实现,然后我尝试了一下:
[root@liqiang.io]# cat main.go
for {
func() {
patchFunc, err = mpatch.PatchMethod(os.Exit, func(code int) {
fmt.Printf("os.Exit(%d) called\n", code)
})
if err != nil {
panic(err)
}
defer func() {
if err := recover(); err != nil {
fmt.Printf("recovered from panic: %v\n", err)
}
patchFunc.Unpatch()
}()
greeter.Greet()
}()
然后发现主程序照样退出了,看上去 Plugin 里面使用的和主程序不是同个符号表?(留个坑后续深入)
Plugin 和主程序通信
留坑。
性能
再来我们就看下 Plugin 的性能如何,按照动态链接库的经验,性能应该是不差了,所以我做了个简单的 Bench:
[root@liqiang.io]# cat bench.go
func BenchmarkNormalFunction(b *testing.B) {
normalGreeter := &greeter{}
for i := 0; i < b.N; i++ {
normalGreeter.Greet()
}
}
func BenchmarkPlugin(b *testing.B) {
greeter, err := reloadMod("./plugins/chinese/greet.so")
if err != nil {
panic(err)
}
for i := 0; i < b.N; i++ {
greeter.Greet()
}
}
然后可以看到结果是,性能几乎一样,所以 Plugin 的性能还是很不错的,几乎无损失。
[root@liqiang.io]# go test -bench=. | grep -v 你好宇宙
goos: linux
goarch: amd64
pkg: github.com/liuliqiang/blog_codes/golang/tools/plugin
cpu: AMD xxxx xxx
2379421 500.3 ns/op
2359544 510.8 ns/op
PASS
ok github.com/liuliqiang/blog_codes/golang/tools/plugin 3.433s