Go 1.8中增加了 plugin package,但是仅支持Linux操作系统,并且还有一些已知的bug。可以说,这个插件系统的实现还未达到"产品级"的水平。
The plugin support is currently incomplete, only supports Linux, and has known bugs.
一些已知的bug已经推到 Go1.10甚至以后的版本中修复了。
今天在测试Go 1.9中的功能的时候就遇到了plugin的一个bug。
按照官方的文档, 开发一个插件很简单:
plugin1/main.go1 2 3 4 5 6 7
| package main import "fmt" var V int func F() { fmt.Printf("Hello, number %d\n", V) }
|
插件中定义了变量V
和方法F
,可以通过下面的命令生成一个so
文件:
1
| go build -buildmode=plugin -o ../p1.so main.go
|
然后通过plugin
包可以加载插件:
main.go1 2 3 4 5 6 7 8 9 10 11 12 13 14
| p, err := plugin.Open("p1.so") if err != nil { panic(err) } v, err := p.Lookup("V") if err != nil { panic(err) } f, err := p.Lookup("F") if err != nil { panic(err) } *v.(*int) = 7 f.(func())()
|
当然作为插件系统,我们希望可以加载新的插件,来替换已有的插件, 如果你复制p1.so
为p2.so
,然后上上面的测试代码中再加载p2.so
会报错:
main.go1 2 3 4 5 6 7 8 9 10 11 12
| p, err := plugin.Open("p1.so") if err != nil { panic(err) } ...... p, err = plugin.Open("p2.so") if err != nil { panic(err) }
|
错误信息如下:
1 2 3 4 5
| go run main.go Hello, number 7 plugin: plugin plugin/unnamed-f0c47a2a99a0d8e8fb40defabb50f238c78f5d58 already loaded fatal error: plugin: plugin already loaded ......
|
这一步我们还能理解,相同的plugin即使文件名更改了,加载进去还是一样的,所以会报plugin already loaded
错误。
我们将plugin1/main.go
中的代码稍微改一下:
plugin1/main.go1 2 3 4 5 6 7
| package main import "fmt" var V int func F() { fmt.Printf("Hello world, %d\n", V) }
|
然后生成插件p2.so
:
1
| go build -buildmode=plugin -o ../p2.so main.go
|
按说这次我们修改了代码,生成了一个新的插件,如果代码同时加载这两个插件,因为没什么问题,但是运行上面的加载两个插件的测试代码,发现还是出错:
1 2 3 4 5
| go run main.go Hello, number 7 plugin: plugin plugin/unnamed-f0c47a2a99a0d8e8fb40defabb50f238c78f5d58 already loaded fatal error: plugin: plugin already loaded ......
|
怪异吧,两个不同代码的生成插件,居然被认为是同一个插件(plugin/unnamed-f0c47a2a99a0d8e8fb40defabb50f238c78f5d58)。
使用nm
查看两个插件的符号表:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| [root@colobu t] 00000000001acfc0 R go.link.pkghashbytes.plugin/unnamed-f0c47a2a99a0d8e8fb40defabb50f238c78f5d58 00000000003f9620 D go.link.pkghash.plugin/unnamed-f0c47a2a99a0d8e8fb40defabb50f238c78f5d58 0000000000199080 t local.plugin/unnamed-f0c47a2a99a0d8e8fb40defabb50f238c78f5d58.F 0000000000199130 t local.plugin/unnamed-f0c47a2a99a0d8e8fb40defabb50f238c78f5d58.init 0000000000199080 T plugin/unnamed-f0c47a2a99a0d8e8fb40defabb50f238c78f5d58.F 0000000000199130 T plugin/unnamed-f0c47a2a99a0d8e8fb40defabb50f238c78f5d58.init 000000000048e027 B plugin/unnamed-f0c47a2a99a0d8e8fb40defabb50f238c78f5d58.initdone· 000000000048e088 B plugin/unnamed-f0c47a2a99a0d8e8fb40defabb50f238c78f5d58.V [root@colobu t] [root@colobu t] [root@colobu t] 00000000001acfc0 R go.link.pkghashbytes.plugin/unnamed-f0c47a2a99a0d8e8fb40defabb50f238c78f5d58 00000000003f9620 D go.link.pkghash.plugin/unnamed-f0c47a2a99a0d8e8fb40defabb50f238c78f5d58 0000000000199080 t local.plugin/unnamed-f0c47a2a99a0d8e8fb40defabb50f238c78f5d58.F 0000000000199130 t local.plugin/unnamed-f0c47a2a99a0d8e8fb40defabb50f238c78f5d58.init 0000000000199080 T plugin/unnamed-f0c47a2a99a0d8e8fb40defabb50f238c78f5d58.F 0000000000199130 T plugin/unnamed-f0c47a2a99a0d8e8fb40defabb50f238c78f5d58.init 000000000048e027 B plugin/unnamed-f0c47a2a99a0d8e8fb40defabb50f238c78f5d58.initdone· 000000000048e088 B plugin/unnamed-f0c47a2a99a0d8e8fb40defabb50f238c78f5d58.V [root@colobu t]
|
可以看到两个插件中生成的符号表符号表是相同的,所以被误认为了同一个插件。
这种情况是在特殊情况下产生的,如果两个插件的文件名不同,或者引用包不同,或者引用的cgo不同,则会生成不同的插件,同时加载不会有问题。但是如果文件名相同,相关的引用也相同,则可能生成相同的插件,尽管插件内包含的方法和变量不同,实现也不同。
这是Go plugin生成的时候一个bug: issue#19358, 期望在Go 1.10中解决,目前的解决办法就是插件的go文件使用不同的名字,或者编译的时候指定pluginpath
:
1 2
| go build -ldflags "-pluginpath=p1"-buildmode=plugin -o ../p1.so main.go go build -ldflags "-pluginpath=p2"-buildmode=plugin -o ../p2.so main.go
|
导致问题的原因正如 LionNatsu 在bug中指出的, Go 判断两个插件是否相同是通过比较pluginpath实现的,如果你在编译的时候指定了不同的pluginpath
,则编译出来的插件是不同的,但是如果没有指定pluginpath
,则由内部的算法生成, 生成的格式为plugin/unnamed-" + root.Package.Internal.BuildID
。
func computeBuildID(p *Package) 生成一个SHA-1的哈希值作为BuildID。
go/src/cmd/go/internal/load/pkg.go1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| func computeBuildID(p *Package) { h := sha1.New() inputFiles := str.StringList( p.GoFiles, p.CgoFiles, p.CFiles, p.CXXFiles, p.MFiles, p.HFiles, p.SFiles, p.SysoFiles, p.SwigFiles, p.SwigCXXFiles, ) for _, file := range inputFiles { fmt.Fprintf(h, "file %s\n", file) } if p.Standard && p.ImportPath == "runtime/internal/sys" && cfg.BuildContext.Compiler != "gccgo" { data, err := ioutil.ReadFile(filepath.Join(p.Dir, "zversion.go")) if err != nil { base.Fatalf("go: %s", err) } fmt.Fprintf(h, "zversion %q\n", string(data)) } for _, p1 := range p.Internal.Deps { fmt.Fprintf(h, "dep %s %s\n", p1.ImportPath, p1.Internal.BuildID) } p.Internal.BuildID = fmt.Sprintf("%x", h.Sum(nil)) }
|
函数的后半部分为Go不同的版本生成不同的哈希,避免用户使用不同的Go版本生成相同的ID。重点看前半部分,可以发现计算哈希的时候只依赖文件名,并不关心文件的内容,这也是我们前面稍微修改一下插件的代码会生成相同的原因, 如果你在代码中import _ "fmt"
也会产生不同的插件。
总之,在Go 1.10之前,为了避免插件冲突, 最好是在编译的时候指定pluginpath
, 比如:
1
| go build -ldflags "-pluginpath=plugin/hot-$(date +%s)" -buildmode=plugin -o hotload.so hotload.go
|
有疑问加站长微信联系(非本文作者)