Go1.8之后支持插件机制,能够动态加载代码。Grafana是开源可视化监控平台,后端是用Go语言编写的,是非常流行的Go语言开源项目,该项目也是基于插件机制,让用户可以下载安装相应的数据库插件。本文介绍插件机制及平台支持情况,如何创建、构建应用以及如何加载插件。

插件机制

Go插件能用于很多场景,基于插件可以把系统分解为通用引擎,容易独立开发和测试。插件都遵循严格接口规范,职责明确。程序可以使用不同插件进行组合,甚至同时使用同一插件的不同版本。主程序和插件之间清晰的界限促进了松耦合和关注点分离。

Go1.8引入新的"plugin"包,提供了Open函数加载共享库返回Plugin对象。插件对象有Lookup函数返回Symbol(interface{}),它可以对插件暴露的函数或变量进行类型断言。官方文档描述Symbol:A Symbol is a pointer to a variable or function.

插件包目前仅支持Linux系统,对于其他OS需要通过其他方式进行实现,官方文档描述如下:

Currently plugins are only supported on Linux, FreeBSD, and macOS. Please report any issues.

插件示例

Go插件与正常包一样,也可以像正常包一样使用它,仅当把它编译为插件才会成为插件。

创建插件

下面实现一个简单功能插件,并在应用中调用其他暴露方法。

package main

import "fmt"

var V int

func F() { 
	fmt.Printf("Hello, number %d\n", V) 
}

非常简单,与正常代码一样,这里暴露了变量V和F()函数。

下面要编译为插件,需要使用-buildmode=plugin选项,并指定名称为typ_plugin.so,放在上级目录,为了后面main中调用:

go build -buildmode=plugin -o ../typ_plugin.so

执行命令,需要安装gcc编译环境,正常会在上级目录生成相应typ_plugin.so库文件。

加载插件

加载插件需要知道目标插件的位置(*.so共享库的位置),可以通过下面几种方法实现:

  • 通过命令行参数指定
  • 设置环境变量
  • 使用配置文件
  • 使用已知目录

另一个问题是主程序是否知道插件名称,或者它是否需要动态地发现某个目录下的所有插件。通过filepath.Glob("plugins/*.so")返回所有.so扩展名的插件,然后调用plugin.Open(filename)加载插件,如果有任何错误,程序报错。

在下面的例子中,程序期望在当前工作目录下有一个名为“plugins”的子目录,并加载它找到的所有插件。
下面示例展示如何加载指定目录下所有插件:

package main
import (
    "fmt"
    "plugin"
    "path/filepath"
)
func main() {
    all_plugins, err := filepath.Glob("plugins/*.so")
    if err != nil {
        panic(err)
    }
    for _, filename := range (all_plugins) {
        fmt.Println(filename)
        p, err := plugin.Open(filename)
        if err != nil {
            panic(err)
        }
    }
}

调用插件

定位并加载插件仅完成了一半,插件对象提供了Lookup方法,给定名称返回接口,然后需要类型断言为具体类型。官方文档描述如下:

Lookup searches for a symbol named symName in plugin p. A symbol is any exported variable or function. It reports an error if the symbol is not found. It is safe for concurrent use by multiple goroutines.

简单描述:基于名称进行查找,查找到返回symbol,否则返回错误。插件支持多个协程并发调用。

下面示例首先加载前面创建的插件,然后查找插件中暴露的变量和方法,然后给变量赋值并调用方法。

package main

import(
    "plugin"
)

func main() {

    p, err := plugin.Open("typ_plugin.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执行类型断言,然后取指针,给其赋值
    *v.(*int) = 8

    // f推理断言为函数并执行
    f.(func())()
}

总结

plugin包给编写复杂Go应用提供了很好的机制,通常编程接口很简单,插件可以基于接口有不同复杂实现。应用动态加载插件,让程序更灵活、易扩展。