译文 Go 语言中的插件

zhuyaguang1368 · 2021年09月07日 · 274 次阅读
本帖已被设为精华帖!

很多年以前我就开始写一系列关于插件的文章:介绍这些插件在不同的系统和编程语言下是如何设计和实现的。今天这篇文章,我打算把这个系列扩展下,讲讲 Go 语言中一些插件的例子。

需要提醒的是,本系列头几篇的文章就介绍了插件的四个基本概念,并且声明几乎所有的插件系统,都可以将它们的设计映射到以下 4 个概念来描述和理解:

  1. 发现

  2. 注册

  3. 插件附着到应用程序上的钩子(又称,” 挂载点 “)

  4. 将应用程序能力暴露给插件(又称,扩展 API)

Gopher holding an Ethernet cable plugged into the wall

两种类型插件

和其他静态编译编程语言一样,Go 中通常会讨论两种一般类型的插件:编译时插件和运行时插件。这两种我们都会讲到。

编译时插件

编译时插件由一系列代码包组成,这些代码包编译进了应用程序的二进制文件中。一旦二进制文件编译好,它的功能就固定了。

最有名的 Go 编译时插件系统就是 database/sql 包的驱动程序。我已经写了一整篇关于这个话题的文章,大家可以看下。

简单概括下:数据库驱动是主应用程序通过一个空白导入 _ "name" 导入的包。这些包通过它们的 init函数使用sql.Registerdatabase/sql注册。

关于基本插件的概念, 下面有一个编译时插件如何运作的例子(以database/sql为例)

  1. 发现:这点很明确,import一个插件包。插件可以在它们init函数自动执行注册。
  2. 注册:由于插件被编译到主应用程序之中,它可以直接从插件中调用一个注册函数 (例如 sql.Register)。
  3. 应用程序钩子:通常,插件将实现应用程序提供的接口,注册过程将连接接口实现。插件使用database/sql实现驱动程序。驱动程序接口和实现该接口的值将使用 sql.Register 注册。
  4. 将应用程序能力暴露给插件:对于编译时插件,这很简单;由于插件被编译成二进制文件,它可以从主应用程序中导入实用程序包,并根据需要在代码中使用它们。

运行时插件

运行时插件的代码不会被编译到主应用程序的原始二进制文件中;相反,它在运行时连接到这个应用程序。在编译语言中,实现这一目标的常用工具是共享库。Go 也支持这种方法。本节的后面部分将提供一个使用共享库,在 Go 中开发插件系统的例子;最后还会讨论其他方式实现的运行时插件。

Go 自带一个内置在标准库中的插件包。这个包让我们可以写出编译进共享库,而不是可执行二进制文件的 Go 程序。另外,它还提供了简单函数来从插件包里面加载共享库和获取符号。

在这篇文章中,我开发了一个完整的运行时插件系统示例;它复制了之前关于插件基础设施的文章中的htmlize源码,并且它的设计和后面那篇C 语言中的插件文章类似。这个示例程序很简单,就是把一些标记语言(比如 reStructuredText 或者 Markdown)转换成 HTML,并支持插件,使得我们能够调整某些标记元素的处理方式。完整的示例代码在这篇文章里。

Directory contents of the plugin sample

让我们用插件的基本概念来分析这个例子。

发现和注册:是通过文件系统查找完成。主应用程序有一个带有LoadPlugins函数的插件包。这个函数扫描给定目录中以.so 结尾的文件,并将所有此类文件视为插件。它希望在每个共享库中找到一个名为InitPlugin的全局函数,并调用它,为它提供一个PluginManager(稍后会详细介绍)。

插件最开始是怎么变成.so文件的呢?通过 命令 -buildmode=plugin 构建。具体更多的细节,可以看示例源码 中的buildplugins.sh脚本和 README 文件。

应用程序勾子:现在是描述PluginManager类型的好时机。这是插件和主应用程序之间通信的主要类型。流程如下:

  • 应用程序在 LoadPlugins 新建一个 PluginManager,并将其传给它找到的所有插件。
  • 每个插件使用PluginManager来给各种勾子注册自己的处理程序。
  • LoadPlugins 在所有的插件注册后,将PluginManager返回给主程序。
  • 当应用程序运行时,使用  PluginManager 来根据需要调用已注册插件的勾子。

举个例子,PluginManager 有下面这个函数:

func (pm *PluginManager) RegisterRoleHook(rolename string, hook RoleHook)

RoleHook 是一个函数类型:

// RoleHook takes the role contents, DB and Post and returns the text this role
// should be replaced with.
type RoleHook func(string, *content.DB, *content.Post) string

插件可以调用RegisterRoleHook 来注册一个特定文本角色的处理程序。请注意,尽管这个设计并没有使用 Go 的 interfaces ,但是其他设计也可以实现同样功能,取决于应用程序的具体情况。

将应用程序能力暴露给插件:正如上面 RoleHook 类型那样,应用程序将数据对象传递给插件使用。content.DB 提供了对应用程序数据库的访问。content.Post 提供了当前格式化插件的特定的 Post。插件可以根据需要使用这些对象,来获取应用程序的数据或者行为。

运行时插件的替代方法

考虑到插件包只是在 Go1.8 中新增的,还有前面描述的种种限制,所以也难怪 Go 生态系统中出现了其他插件方法。

其中最有趣的一个方向就是,IMHO,通过 RPC 调用插件。我一直很喜欢将应用程序解耦到独立的进程中,然后通过 RPC 或本地主机上的 TCP 进行通信。(我猜他们现在称之为 微服务),因为它有几个重要的优点:

  • 隔离性:插件的崩溃不会导致整个应用程序崩溃。
  • 语言之间的交互性:如果 RPC 是接口,你还会在乎插件使用什么语言写的吗?
  • 分布式:如果插件通过网络接口,我们可以很容易将它们分发到不同机器上,来提高性能、可靠性等等。

另外,Go 标准库中有一个很强大的 RPC 包:net/rpc,让这一点实现起来相当容易。

最广泛使用的基于 RPC 的插件系统就是hashicorp/go-plugin,Hashicorp 以创建优秀的 Go 软件而闻名,显然他们在许多系统中使用了 Go 插件,因此这些插件都是经过实战测试过的。(尽管他们的文档可以写的更好点)

Go 插件运行在 net/rpc 之上,当然也支持 gRPC。像 gRPC 这样的高级 RPC 协议非常适合插件,因为它们包含了开箱即用的版本控制,解决了不同版本的插件与主应用程序之间的互操作性问题。

更多原创文章干货分享,请关注公众号
  • 加微信实战群请加微信(注明:实战群):gocnio
astaxie 将本帖设为了精华贴 09月07日 12:33
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册