概述

最近 yiran 推荐了一个 Go 语言的 plugin 机制,虽然这个东西在 1.8 版本就有了,但是出于孤陋寡闻一直都不知道,然后就搜索了一下,发现居然就是以前在 C/C++ 中的动态链接库,马上就懂了,这里就展示一下如何在 Go 中创建并使用 plugin(动态链接库)。

这里我参考的是 Github 上的一个开源项目 vladimirvivien/go-plugin-example,这个示例演示的是如何通过动态加载不同的链接库,从而实现在不修改代码的情况下,使用不同版本的代码,这个特性在热更新中特别有用。

并且我做了一些额外的测试,下面会介绍我做过的这些测试,完整代码你可以在我的 Blog Codes Repo 中找到。

使用方式

Go plugin 的用法相对也比较简单,可以这么拆分为 3 步:

1. 加载动态库

  1. [root@liqiang.io]# cat main.go
  2. import "plugin"
  3. mod := "./chinese/greet.so"
  4. plug, err := plugin.Open(mod)
  5. if err != nil {
  6. panic(err)
  7. }

直接通过 import "plugin",然后使用 plugin.Open 即可加载动态库了。

2. 查找目标函数(变量)

  1. symGreeter, err := plug.Lookup("Greeter")
  2. if err != nil {
  3. panic(err)
  4. }

这里可以通过在加载进来的动态库中寻找目标函数,但是加载进来的目标函数的类型是:Symbol 类型,所以得转换一下。

3. 转换目标函数

  1. var greeter Greeter
  2. greeter, ok := symGreeter.(Greeter)
  3. if !ok {
  4. fmt.Println("unexpected type from module symbol")
  5. os.Exit(1)
  6. }

然后就可以直接调用了:greeter.Greet(),整个结构就是这么简单。

一些尝试

运行过程中(已加载完毕),动态库被删除

那么如果运行过程中,动态库被删除了会怎样?所以我决定试验一下:

  1. [root@liqiang.io shell1]# go run greeter.go -lang chinese
  2. 你好宇宙
  3. 你好宇宙
  4. 你好宇宙 <---- chi.so 已被删除,不影响代码运行
  5. 你好宇宙
  6. 你好宇宙

运行过程中动态更换

为了验证动态替换的效果,我修改了一下原来的代码,加了一个 Http Server(你也可以用 signal 代替),然后动态修改语言,例如:

  1. greeter := lookupGreeter(mod)
  2. http.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) {
  3. mod = "./eng/eng.so"
  4. greeter = lookupGreeter(mod)
  5. writer.Write([]byte("ok"))
  6. })
  7. go func() {
  8. http.ListenAndServe(":8080", nil)
  9. }()
  10. for {
  11. greeter.Greet()
  12. time.Sleep(time.Second)
  13. }

然后我以 chinese 为参数启动,在运行过程中修改语言:

  1. go run greeter.go -lang chinese
  2. 你好宇宙
  3. 你好宇宙
  4. 你好宇宙
  5. Hello Universe <---- 修改语言,成功修改
  6. Hello Universe
  7. Hello Universe

然后可以看到,效果是成功的!

运行过程中,plugin 的函数 panic

作为一个 Plugin,我们最担心的就是 plugin 中的程序异常了,会不会导致我们的主程序异常,所以我尝试在 Plugin 的代码中使用了 Panic,然后再主程序中进行捕获这个 panic:

Plugin 的代码:

  1. [root@liqiang.io]# cat panic/greet.go
  2. func (g greeting) Greet() {
  3. panic("I'm gone.")
  4. }

主程序代码:

  1. [root@liqiang.io]# cat main.go
  2. for {
  3. func() {
  4. defer func() {
  5. if err := recover(); err != nil {
  6. fmt.Printf("recovered from panic: %v\n", err)
  7. }
  8. }()
  9. greeter.Greet()
  10. }()
  11. time.Sleep(time.Second)
  12. }

然后在运行的时候,我分别替换为 panic 版本和中文版本的插件:

  1. 运行代码,等待几秒
  2. 替换成 panic 版本:curl http://localhost:8080\?mod\=panic
  3. 替换成中文版本:curl http://localhost:8080\?mod\=chinese

然后可以看到输出是这样的:

  1. [root@liqiang.io]# go run ./main.go
  2. Hello Universe
  3. Hello Universe
  4. Hello Universe
  5. recovered from panic: I'm gone.
  6. recovered from panic: I'm gone.
  7. recovered from panic: I'm gone.
  8. 你好宇宙
  9. 你好宇宙

结论就是可以容许 Panic。

运行过程中 Plugin 调用 os.Exit

既然 panic 是可以容许的,那么如果 Plugin 调用了 os.Exit(1) 会怎么样?会不会导致主程序也被迫退出了,于是我写了一个新的 Plugin 代码:

  1. [root@liqiang.io]# cat exit/greet.go
  2. package main
  3. import "os"
  4. type greeting string
  5. func (g greeting) Greet() {
  6. os.Exit(1)
  7. }

然后执行代码,再替换成这个 exit 的版本,然后发现,主程序退出了!这很危险,如果 Plugin 的代码写的不好,那很容易就危及到了主程序的正常运行,所以这个还是需要担心一下的,那么有没有方法避免呢?我第一个想法就是替换 os.Exit 的实现,然后我尝试了一下:

  1. [root@liqiang.io]# cat main.go
  2. for {
  3. func() {
  4. patchFunc, err = mpatch.PatchMethod(os.Exit, func(code int) {
  5. fmt.Printf("os.Exit(%d) called\n", code)
  6. })
  7. if err != nil {
  8. panic(err)
  9. }
  10. defer func() {
  11. if err := recover(); err != nil {
  12. fmt.Printf("recovered from panic: %v\n", err)
  13. }
  14. patchFunc.Unpatch()
  15. }()
  16. greeter.Greet()
  17. }()

然后发现主程序照样退出了,看上去 Plugin 里面使用的和主程序不是同个符号表?(留个坑后续深入)

Plugin 和主程序通信

留坑。

性能

再来我们就看下 Plugin 的性能如何,按照动态链接库的经验,性能应该是不差了,所以我做了个简单的 Bench:

  1. [root@liqiang.io]# cat bench.go
  2. func BenchmarkNormalFunction(b *testing.B) {
  3. normalGreeter := &greeter{}
  4. for i := 0; i < b.N; i++ {
  5. normalGreeter.Greet()
  6. }
  7. }
  8. func BenchmarkPlugin(b *testing.B) {
  9. greeter, err := reloadMod("./plugins/chinese/greet.so")
  10. if err != nil {
  11. panic(err)
  12. }
  13. for i := 0; i < b.N; i++ {
  14. greeter.Greet()
  15. }
  16. }

然后可以看到结果是,性能几乎一样,所以 Plugin 的性能还是很不错的,几乎无损失。

  1. [root@liqiang.io]# go test -bench=. | grep -v 你好宇宙
  2. goos: linux
  3. goarch: amd64
  4. pkg: github.com/liuliqiang/blog_codes/golang/tools/plugin
  5. cpu: AMD xxxx xxx
  6. 2379421 500.3 ns/op
  7. 2359544 510.8 ns/op
  8. PASS
  9. ok github.com/liuliqiang/blog_codes/golang/tools/plugin 3.433s

Ref