一个基于Go插件的系统的惯用方法

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

Idiomatic approach to a Go plugin-based system

问题

我有一个Go项目,我想开源,但有一些元素不适合开源,例如公司特定的逻辑等。

我构思了以下方法:

  • 在核心存储库中定义interface
  • 插件可以是独立的存储库,其type实现了核心中定义的interface。这样可以将插件放在完全独立的模块中,因此可以有自己的CI作业等。
  • 通过符号链接将插件编译到最终的二进制文件中。

这将导致类似以下的目录结构:

|- $GOPATH
  |- src
    |- github.com
      |- jabclab
        |- core-system
          |- plugins <-----|
      |- xxx               | 
        |- plugin-a ------>| ln -s
      |- yyy               |  
        |- plugin-b ------>|

示例工作流程如下:

<!-- language: lang-none -->

$ go get git@github.com:jabclab/core-system.git
$ go get git@github.com:xxx/plugin-a.git
$ go get git@github.com:yyy/plugin-b.git
$ cd $GOPATH/src/github.com
$ ln -s ./xxx/plugin-a/*.go ./jabclab/core-system/plugins
$ ln -s ./yyy/plugin-b/*.go ./jabclab/core-system/plugins
$ cd jabclab/core-system
$ go build

我不确定的一个问题是如何在核心中在运行时使用插件中定义的类型。我不想使用reflect,但目前想不到更好的方法。如果我在一个存储库中编写代码,我会使用类似以下的方式:

package plugins

type Plugin interface {
  Exec(chan&lt;- string) error
}

var Registry map[string]Plugin

// plugin_a.go
func init() { Registry[&quot;plugin_a&quot;] = PluginA{} }

// plugin_b.go
func init() { Registry[&quot;plugin_b&quot;] = PluginB{} }

除了上述问题,这种整体方法是否被认为是符合惯用方式的?

英文:

I have a Go project I would like to open source but there are certain elements which are not suitable for OSS, e.g. company specific logic etc.

I have conceived of the following approach:

  • interfaces are defined in the core repository.
  • Plugins can then be standalone repositories whose types implement the interfaces defined in core. This allows the plugins to be housed in completely separate modules and therefore have their own CI jobs etc.
  • Plugins are compiled into the final binary via symlinks.

This would result in a directory structure something like the following:

|- $GOPATH
  |- src
    |- github.com
      |- jabclab
        |- core-system
          |- plugins &lt;-----|
      |- xxx               | 
        |- plugin-a ------&gt;| ln -s
      |- yyy               |  
        |- plugin-b ------&gt;|

With an example workflow of:

<!-- language: lang-none -->

$ go get git@github.com:jabclab/core-system.git
$ go get git@github.com:xxx/plugin-a.git
$ go get git@github.com:yyy/plugin-b.git
$ cd $GOPATH/src/github.com
$ ln -s ./xxx/plugin-a/*.go ./jabclab/core-system/plugins
$ ln -s ./yyy/plugin-b/*.go ./jabclab/core-system/plugins
$ cd jabclab/core-system
$ go build

The one issue I'm not sure about is how to make the types defined in plugins available at runtime in core. I'd rather not use reflect but can't think of a better way at the moment. If I was doing the code in one repo I would use something like:

package plugins

type Plugin interface {
  Exec(chan&lt;- string) error
}

var Registry map[string]Plugin

// plugin_a.go
func init() { Registry[&quot;plugin_a&quot;] = PluginA{} }

// plugin_b.go
func init() { Registry[&quot;plugin_b&quot;] = PluginB{} }

In addition to the above question would this overall approach be considered idiomatic?

答案1

得分: 5

这是我在Go中最喜欢的问题之一。我有一个开源项目也需要处理这个问题(https://github.com/cpg1111/maestrod),它具有可插拔的数据库和运行时(Docker、k8s、Mesos等)客户端。在Go的master分支中有一个插件包(应该很快会发布稳定版本),在此之前,我只是将所有插件编译到二进制文件中,并通过配置来决定使用哪个。

关于插件包,可以使用动态链接来加载插件,类似于C语言中的dlopen(),Go的插件包的行为在文档中有很好的说明。

此外,我建议看一下Hashicorp是如何通过在本地Unix套接字上进行RPC来解决这个问题的。https://github.com/hashicorp/go-plugin

像Hashicorp的模型一样,将插件作为一个独立的进程运行的额外好处是,如果插件失败,但主进程能够处理该失败,你将获得很好的稳定性。

我还应该提到,Docker也以类似的方式在Go中使用插件,只是Docker使用HTTP而不是RPC。此外,一个Docker工程师曾经在过去的文章中提到过嵌入JavaScript解释器以实现动态逻辑的方法http://crosbymichael.com/category/go.html。

我想指出的是,在评论中提到的sql包的模式存在问题,那并不是真正的插件架构,你仍然受限于你的导入包,所以你可以有多个main.go文件,但那不是插件,插件的目的是使同一个程序可以运行一段代码或另一段代码。像sql包这样的东西只是提供了灵活性,一个单独的包决定使用哪个数据库驱动程序。尽管如此,你最终还是需要修改代码来更改使用的驱动程序。

我还想补充一点,在所有这些插件模式中,除了将其编译到同一个二进制文件中并使用配置进行选择之外,每个插件都可以有自己的构建、测试和部署(即它们自己的CI/CD),但不一定需要这样做。

英文:

This is one of my favorite issues in Go. I have an open source project that has to deal with this as well (https://github.com/cpg1111/maestrod), it has pluggable DB and Runtime (Docker, k8s, Mesos, etc) clients. Prior to the plugin package that is in the master branch of Go (so it should be coming to a stable release soon) I just compiled all of the plugins into the binary and allowed configuration decide which to use.

As of the plugin package, https://tip.golang.org/pkg/plugin/, you can use dynamic linking for plugins, so similar to C's dlopen() in its loading, and the behavior of go's plugin package is pretty well outlined in the documentation.

Additionally I recommend taking a look at how Hashicorp addresses this by doing RPC over a local unix socket. https://github.com/hashicorp/go-plugin

The added benefit of running a plugin as a separate process like Hashicorp's model does, is that you get great stability in the event that the plugin fails but the main process is able to handle that failure.

I should also mention Docker does its plugins in Go similarly, except Docker uses HTTP instead of RPC. Additionally, a Docker engineer has posted about embedding a Javascript interpreter for dynamic logic in the past http://crosbymichael.com/category/go.html.

The issue I wanted to point out with the sql package's pattern that was mentioned in the comments is that that's not a plugin architecture really, you're still limited to whatever is in your imports, so you could have multiple main.go's but that's not a plugin, the point of a plugin is such that the same program can run either one piece of code or another. What you have with things like the sql package is flexibility where a separate package determines what DB driver to use. Nonetheless, you end up modifying code to change what driver you are using.

I want to add, with all of these plugin patterns, aside from the compiling into the same binary and using configuration to choose, each can have its own build, test and deployment (i.e. their own CI/CD) but not necessarily.

huangapple
  • 本文由 发表于 2016年3月1日 04:05:49
  • 转载请务必保留本文链接:https://go.coder-hub.com/35708608.html
匿名

发表评论

匿名网友

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

确定