Go插件依赖项是如何工作的?

huangapple go评论74阅读模式
英文:

How do Go plugin dependencies work?

问题

Go 1.8支持Go插件。

我创建了两个插件,如下所示。

据我了解,插件只公开main包中的函数和变量。也就是说,对于非main变量/函数,plugin.Lookup()将失败。

但是,我想测试一下插件是否可以在内部调用另一个插件的方法,类似于C++库调用另一个库的方式。

所以我进行了以下测试:

插件1 github.com/vimal/testplugin

$ cat myplugin.go
package main

import "C"
import "fmt"
import help "github.com/vimal/testplugin1/plug"

func init() {
        fmt.Printf("main.init invoked\n")
}
// TestPlugin 
func TestPlugin() string {
        return help.Help()
}

插件2 github.com/vimal/testplugin1

$ cat myplugin.go
package main

import "C"

func HelperFunc() string {
        return "help"
}
$ cat plug/helper.go
package help

func Help() string {
        return "help234"
}

这里的想法是插件1调用插件2的内部非main函数。

主程序

主程序加载一些作为参数给出的插件,并从最后一个插件调用TestPlugin()

测试1:

构建两个插件,加载两个插件,并调用TestPlugin(),输出包含"help234",即内部函数被调用。这可以理解,因为两个插件都被加载,一个插件可以调用另一个插件的内部代码。

测试2:

只加载插件1,并调用TestPlugin(),输出包含"help234",即内部函数被调用。与测试1中观察到的输出相同。也许这次方法是从GOPATH中找到的。

测试3:

将文件夹"github.com/vimal/testplugin1"重命名为"github.com/vimal/junk1",删除插件2,只加载插件1,并调用TestPlugin()。输出仍然包含"help234",即内部函数被调用。

我无法理解为什么测试3会产生相同的输出。插件1是否包含插件2的代码?我如何理解Go插件对其他Go插件的依赖关系?

Go版本:go version go1.8rc3 linux/amd64

英文:

Go 1.8 supports Go plugins.

I have created two plugins as follows.

As I understand, the plugin exposes only the functions and variables in the main package. i.e. plugin.Lookup() will fail for non-main variable/function.

But I wanted to test if a plugin can internally invoke a method from another plugin, similar to how a C++ library can invoke another library.

So I tested as follows:

plugin1 github.com/vimal/testplugin

$ cat myplugin.go
package main

import "C"
import "fmt"
import help "github.com/vimal/testplugin1/plug"

func init() {
        fmt.Printf("main.init invoked\n")
}
// TestPlugin 
func TestPlugin() string {
        return help.Help()
}

plugin2 github.com/vimal/testplugin1

$ cat myplugin.go
package main

import "C"

func HelperFunc() string {
        return "help"
}
$ cat plug/helper.go
package help

func Help() string {
        return "help234"
}

Idea here is that plugin1 invokes an internal, non-main function of plugin2.

main program

Main program loads a number of plugins given as arguments, and invokes TestPlugin() from the last plugin.

test 1:

Build both plugins, and load both plugins, and invoke TestPlugin(), the output contains "help234", i.e. the inner function is invoked. This can be understood, since both plugins are loaded, one plugin can invoke another plugin's inner code.

test 2:

Load only plugin1, and invoke TestPlugin(), the output contains "help234", i.e. the inner function is invoked. The same output is observed as in test1. Perhaps this time the method is found from GOPATH.

test 3:

Rename the folder "github.com/vimal/testplugin1" to "github.com/vimal/junk1", delete the plugin2, and load only plugin1, and invoke TestPlugin(). the output still contains "help234", i.e. the inner function is invoked.

I am not able to understand how test3 produces the same output. Does plugin1 contain plugin2 code also? How can I understand the Go plugin dependency on other Go plugins?

Go version : go version go1.8rc3 linux/amd64

答案1

得分: 11

你并没有完全按照你所认为的方式进行操作。

你的plugin1导入并使用了一个包,即github.com/vimal/testplugin1/plug。这与plugin2并不相等!

这里发生的情况是,当你构建plugin1时,它的所有依赖项都会被构建到插件文件中,包括.../testplugin1/plug包。当你加载plugin1时,它的所有依赖项也会从插件文件中加载,包括plug包。因此,不论plugin2的加载状态如何,它都能正常工作。这两个插件是相互独立的。

-buildmode=plugin指示编译器构建一个插件而不是独立的应用程序,但这并不意味着依赖项不需要被包含进来。它们必须被包含进来,因为插件无法保证哪个Go应用程序将加载它,以及该Go应用程序将具有哪些包。因为一个可运行的应用程序只包含它自己明确引用的包,甚至包括标准库中的包。

确保插件拥有所需的一切并且能够正常工作的唯一方法是包含它的所有依赖项,包括标准库中的依赖项。(这就是为什么构建简单插件会生成相对较大的文件的原因,类似于构建简单的Go可执行文件会生成大文件。)

不需要添加到插件中的一些内容包括Go运行时,因为加载插件的运行中的Go应用程序已经有一个正在运行的Go运行时。(请注意,只能从使用相同Go版本编译的应用程序中加载插件。)但除此之外,插件必须包含它所需的一切。

Go是一种静态链接语言。一旦Go应用程序或插件被编译,它们不依赖于也不检查GOPATH的值,它只在构建它们时由Go工具使用。

更深入的了解

可能你的主应用程序和一个插件引用了相同的包(按导入路径来说是“相同的”)。在这种情况下,只会使用一个包的“实例”。

如果这个常用的包有“状态”,比如一个全局变量,可以进行测试。假设有一个名为mymath的常用共享包:

package mymath

var S string

func SetS(s string) {
    S = s
}

还有一个使用它的插件pg

package main

import (
    "C"
    "mymath"
    "fmt"
)

func Start() {
    fmt.Println("pg:mymath.S", mymath.S)
    mymath.SetS("pghi")
    fmt.Println("pg:mymath.S", mymath.S)
}

以及使用mymath并加载pg(使用它)的主应用程序:

package main

import (
    "plugin"
    "mymath"
    "fmt"
)

func main() {
    fmt.Println("mymath.S", mymath.S)
    mymath.SetS("hi")
    fmt.Println("mymath.S", mymath.S)

    p, err := plugin.Open("../pg/pg.so")
    if err != nil {
        panic(err)
    }

    start, err := p.Lookup("Start")
    if err != nil {
        panic(err)
    }

    start.(func())()

    fmt.Println("mymath.S", mymath.S)
}

构建插件:

cd pg
go build -buildmode=plugin

运行主应用程序,输出结果为:

mymath.S 
mymath.S hi
pg:mymath.S hi
pg:mymath.S pghi
mymath.S pghi

分析:首先,主应用程序操作mymath.S,将其设置为"hi"。然后是插件,它打印出它(我们看到的是主应用程序设置的值"hi"),然后将其更改为"pghi"。然后再次是主应用程序,打印mymath.S,再次看到插件设置的最后一个值:"pghi"

因此,mymath只有一个“实例”。现在,如果你继续修改mymath,例如将myMath.SetS()重命名为mymath.SetS2(),并在主应用程序中更新调用(mymath.SetS2("hi")),而不重新构建插件,只运行主应用程序,你将得到以下输出:

mymath.S 
mymath.S hi
panic: plugin.Open: plugin was built with a different version of package mymath

goroutine 1 [running]:
main.main()
    <GOPATH>/src/play/play.go:16 +0x4b5
exit status 2

如你所见,当构建主应用程序和插件时,记录了包版本(很可能是一个哈希值),如果主应用程序和插件中的导入路径匹配,这些版本必须匹配。

(请注意,如果你只更改了使用的mymath包的导出标识符(和签名),而不更改其实现,例如func SetS(s string) { S = s + "+" },你也会得到上述错误。)

英文:

You're not doing exactly what you think you are.

Your plugin1 imports and uses a package, namely github.com/vimal/testplugin1/plug. This is not "equal" to plugin2!

What happens here is that when you build plugin1, all its dependencies are built into the plugin file, including the .../testplugin1/plug package. And when you load plugin1, all its dependencies are also loaded from the plugin file, including the plug package. After this, it's not surprising that it works no matter the loaded status of plugin2. These 2 plugins are independent from each another.

The -buildmode=plugin instructs the compiler that you want to build a plugin and not a standalone app, but it doesn't mean dependencies must not be included. They have to be, because the plugin cannot have any guarantee what Go app will load it, and what packages that Go app will have. Because a runnable app also only contains packages even from the standard library that the app itself references explicitly.

The only way to guarantee that the plugin will have everything it needs and so that it will work if it also contains all its dependencies, including those from the standard library. (This is the reason why building simple plugins generate relatively big files, similarly to building simple Go executables resulting in big files.)

Few things that don't need to be added to plugins include the Go runtime for example, because a running Go app that will load the plugin will already have a Go runtime running. (Note that you can only load a plugin from an app compiled with the same version of Go.) But beyond that, the plugin has to contain everything it needs.

Go is a statically linked language. Once a Go app or plugin is compiled, they do not rely nor check the value of GOPATH, it is only used by the Go tool during building them.

Deeper insight

It's possible that your main app and a plugin refers to the same package ("same" by import path). In such cases only one "instance" of the package will be used.

This can be tested if this commonly referred package has "state", e.g a global variable. Let's assume a common, shared package called mymath:

package mymath

var S string

func SetS(s string) {
    S = s
}

And a plugin called pg that uses it:

package main

import (
	&quot;C&quot;
	&quot;mymath&quot;
	&quot;fmt&quot;
)

func Start() {
	fmt.Println(&quot;pg:mymath.S&quot;, mymath.S)
	mymath.SetS(&quot;pghi&quot;)
	fmt.Println(&quot;pg:mymath.S&quot;, mymath.S)
}

And the main app that uses mymath and loads pg (which uses it):

package main

import (
	&quot;plugin&quot;
	&quot;mymath&quot;
	&quot;fmt&quot;
)

func main() {
	fmt.Println(&quot;mymath.S&quot;, mymath.S)
	mymath.SetS(&quot;hi&quot;)
	fmt.Println(&quot;mymath.S&quot;, mymath.S)

	p, err := plugin.Open(&quot;../pg/pg.so&quot;)
	if err != nil {
		panic(err)
	}

	start, err := p.Lookup(&quot;Start&quot;)
	if err != nil {
		panic(err)
	}

	start.(func())()

	fmt.Println(&quot;mymath.S&quot;, mymath.S)
}

Building the plugin:

cd pg
go build -buildmode=plugin

Running the main app, the output is:

mymath.S 
mymath.S hi
pg:mymath.S hi
pg:mymath.S pghi
mymath.S pghi

Analysis: first the main app plays with mymath.S, sets it to &quot;hi&quot; eventually. Then comes the plugin, which prints it (we see the value set by the main app &quot;hi&quot;), then changes it to &quot;pghi&quot;. Then comes again the main app and prints mymath.S, and again, sees the last value set by the plugin: &quot;pghi&quot;.

So there is only one "instance" of mymath. Now if you go ahead and change mymath, e.g. rename myMath.SetS() to mymath.SetS2(), and you update the call in the main app (to mymath.SetS2(&quot;hi&quot;)), and without rebuilding the plugin, just running the main app, you get the following output:

mymath.S 
mymath.S hi
panic: plugin.Open: plugin was built with a different version of package mymath

goroutine 1 [running]:
main.main()
	&lt;GOPATH&gt;/src/play/play.go:16 +0x4b5
exit status 2

As you can see, when building the main app and the plugin, the package version (which is most likely a hash) is recorded, which must match if their import paths match in the main app and in the plugin.

(Note that you will also get the above error if you don't change the exported identifiers (and signatures) of the used mymath package, only the implementation; e.g. func SetS(s string) { S = s + &quot;+&quot; }.)

huangapple
  • 本文由 发表于 2017年2月14日 13:22:57
  • 转载请务必保留本文链接:https://go.coder-hub.com/42218472.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定