Go 插件化开发

Golang 插件化开发

Golang官方提供了plugin模块,该模块可以支持插件开发.

目前很多思路都是在开发过程中支持插件话,当主体程序写完后,不能够临时绑定插件.但是本文将带领你进行主体程序自动识别并加载、控制插件调用.

代码地址: https://github.com/A-Donga/PluginTest

@[toc]

基本思路

插件化开发中,一定存在一个主体程序,对其他插件进行控制、处理、调度.

具有模拟业务的主体程序

我们首先开发一个简单的业务程序,进行两种输出.

  1. 当时间秒数为奇数的时候,输出hello

  2. 当时间秒数为偶数的时候,输出world

主体代码

代码有一定的冗余,是为了模拟业务之间的调度

主文件名:MainFile.go


package main

import (

"fmt"

"time"

)

/*

@author: mxd

@create time: 2020/10/5

*/

// main 主体程序入口

func main() {

// time.Now().Second 将会返回当前秒数

nowSecond := time.Now().Second()

doPrint(nowSecond)

fmt.Println("Process Stop ========")

}

// 执行打印操作

func doPrint(nowSecond int) {

if nowSecond%2 == 0 {

printWorld() //偶数

} else {

printHello() //奇数

}

}

// 执行打印hello

func printHello() {

fmt.Println("hello")

}

// 执行打印world

func printWorld() {

fmt.Println("world")

}

// init 函数将于 main 函数之前运行

func init() {

fmt.Println("Process On ==========")

}

输出如下:[图片上传失败...(image-e5424a-1602289163376)]

简单的插件

然后我们编写一个插件代码

插件代码的入口package也要为main,但是可以不包含main方法

设定插件逻辑为当当前秒数为奇数的时候,同时输出当前时间(与hello的判定不是一个时间)

插件文件名:HelloPlugin.go

插件代码

package main

import (

"fmt"

"time"

)

/*

@author: mxd

@create time: 2020/10/5

*/

// 打印当前时间

func PrintNowTime(){

fmt.Println(time.Now().Second())

}

在当前目录下,执行插件生成指令:


$ go build --buildmode=plugin -o HelloPlugin.so HelloPlugin.go

当前目录下就会多出来一个文件HelloPlugin.so

然后,我们让主程序加载该插件

修改的主体代码

package main

import (

"fmt"

"plugin"

"time"

)

/*

@author: mxd

@create time: 2020/10/5

*/

// main 主体程序入口

func main() {

// time.Now().Second 将会返回当前秒数

nowSecond := time.Now().Second()

doPrint(nowSecond)

fmt.Println("Process Stop ========")

}

// 执行打印操作

func doPrint(nowSecond int) {

if nowSecond%2 == 0 {

printWorld() //偶数

} else {

printHello() //奇数

}

}

// 执行打印hello

func printHello() {

// 执行插件调用

if pluginFunc != nil{

//将存储的信息转换为函数

if targetFunc, ok := pluginFunc.(func()); ok {

targetFunc()

}

}

fmt.Println("hello")

}

// 执行打印world

func printWorld() {

fmt.Println("world")

}

// 定义插件信息

const pluginFile = "HelloPlugin.so"

// 存储插件中将要被调用的方法或变量

var pluginFunc plugin.Symbol

// init 函数将于 main 函数之前运行

func init() {

// 查找插件文件

pluginFile, err := plugin.Open(pluginFile)

if err != nil {

fmt.Println("An error occurred while opening the plug-in")

} else{

// 查找目标函数

targetFunc, err := pluginFile.Lookup("PrintNowTime")

if err != nil {

fmt.Println("An error occurred while search target func")

}

pluginFunc = targetFunc

}

fmt.Println("Process On ==========")

}

运行效果如下

插件化代码

如上,我们的主体文件已经写好,我们不需要再修改生成后的可执行文件,如果需要扩展代码,仅需要修改插件代码,然后生成so文件替换即可.

插件进阶-批量化

批量化

我们需要考虑到一个问题,如果我们要支持很多的插件,一个一个写的化,很容易导致我们的主体文件膨胀,因为我们将插件文件写死,无法完成自动识别,因此,我们要为主体文件提供自动识别的功能,自动加载插件

自动读取文件夹下的插件

我们可以单独设置一个名为plugins的文件夹来保存所有插件

首先我么在项目根目录创建一个文件夹plugins

我们将刚刚写好的插件代码移动到 plugins文件夹下,同时为了符合golang标准布局,我们将主文件移动到cmd文件夹下. 此时项目目录如下:

项目目录结构

然后,在项目跟(与cmd、plugins同级)目录下新建一个文件,用来处理与业务无关的util.go代码.


package PluginTest

import (

"fmt"

"io/ioutil"

"path"

)

/*

@author: mxd

@create time: 2020/10/5

*/

//FindFile 将会打开指定目录,并返回该目录下的所有文件

func FindFile(directoryPath string) []string {

// 尝试打开文件夹

baseFile, err := ioutil.ReadDir(directoryPath)

if err != nil {

fmt.Println("An error occurred while open file :[" + directoryPath + "] .")

fmt.Println(err)

return nil

}

// 定义返回数据

var res []string

for _, fileItem := range baseFile {

// 文件夹类型继续递归查找

if fileItem.IsDir() {

// 加上前缀路径,合成正确的相对或绝对路径

innerFiles := FindFile(path.Join(directoryPath, fileItem.Name()))

// 合并结果集

res = append(res, innerFiles...)

} else {

// 这里可以添加过滤,但是会提高方法的复杂度

/*

if path.Ext(fileItem.Name()) == ".so"{

...

}

*/

res = append(res, path.Join(directoryPath, fileItem.Name()))

}

}

return res

}

然后修改MainFile文件,让主文件读取插件文件夹

MainFile更新代码如下

package main

import (

"PluginTest"

"fmt"

"time"

)

/*

@author: mxd

@create time: 2020/10/5

*/

// main 主体程序入口

func main() {

// time.Now().Second 将会返回当前秒数

nowSecond := time.Now().Second()

doPrint(nowSecond)

fmt.Println("Process Stop ========")

}

// 执行打印操作

func doPrint(nowSecond int) {

if nowSecond%2 == 0 {

printWorld() //偶数

} else {

printHello() //奇数

}

}

// 执行打印hello

func printHello() {

fmt.Println("hello")

}

// 执行打印world

func printWorld() {

fmt.Println("world")

}

// init 函数将于 main 函数之前运行

func init() {

// 读取plugin文件夹

pluginsFiles := PluginTest.FindFile("plugins")

for _, pluginItem := range(pluginsFiles){

fmt.Println(pluginItem)

}

fmt.Println("Process On ==========")

}

运行结果如下

在这里插入图片描述

插件装载

插件装载很简单,但是让插件运行需要们指定一个函数,所有插件都要必须实现该方法,但是如果批量后,我们无法确定插件的运行时机,因此我们会在装载后,直接运行插件,测试我们的批量装载是可行的.

首先我们需要创建一个单独处理插件的文件pluginSupport.go

插件支持代码如下

package PluginTest

import (

"fmt"

"path"

"plugin"

)

/*

@author: mxd

@create time: 2020/10/5

*/

// PluginItem 存储着插件的信息

type PluginItem struct {

Name      string

TargetFunc plugin.Symbol

}

// 所有插件必须实现该方法

const TargetFuncName = "TargetFunc"

// LoadAllPlugin 将会过滤一次传入的targetFile,同时将so后缀的文件装载,并返回一个插件信息集合

func LoadAllPlugin(targetFile []string) []PluginItem {

var res []PluginItem

for _, fileItem := range targetFile {

// 过滤插件文件

if path.Ext(fileItem) == "so" {

pluginFile, err := plugin.Open(fileItem)

if err != nil {

fmt.Println("An error occurred while load plugin : [" + fileItem + "]")

fmt.Println(err)

}

//查找指定函数或符号

targetFunc, err := pluginFile.Lookup(TargetFuncName)

if err != nil {

fmt.Println("An error occurred while search target func : [" + fileItem + "]")

fmt.Println(err)

}

//采集插件信息

pluginInfo := PluginItem{

Name:      fileItem,

TargetFunc: targetFunc,

}

// 进行调用

if f, ok := targetFunc.(func()); ok {

f()

}

res = append(res, pluginInfo)

}

}

return res

}

修改main函数,使主函数支持该逻辑调用


// init 函数将于 main 函数之前运行

func init() {

// 读取plugin文件夹

pluginsFiles := PluginTest.FindFile("plugins")

// 装载插件

pluginItems := PluginTest.LoadAllPlugin(pluginsFiles)

fmt.Println(pluginItems)

fmt.Println("Process On ==========")

}

修改插件代码,使其具有TargetFunc方法


package main

import (

"fmt"

"time"

)

/*

@author: mxd

@create time: 2020/10/5

*/

func TargetFunc(){

PrintNowTime()

}

// 打印当前时间

func PrintNowTime(){

fmt.Println(time.Now().Second())

}

生成so文件


$ cd plugins

$ go build --buildmode=plugin -o HelloPlugin.so HelloPlugin.go

然后运行主文件

批量插件化开发

增加插件进行测试

增加插件,但是不修改主代码逻辑

该插件实现逻辑如下:

  1. 当前时间秒数< 30 : 打印

  2. 当前时间秒数>=30: 打印

创建文件:TestPlugin.go

由于两个代码中都含有同样的方法TargetFunc,编辑器会报错,所以将HelloPlugin.go文件中的相关代码注释掉即可(不执行go build命令)

实际开发过程中,插件和主体程序是不会混在一起的,但是这里考虑方便才写到一起的

代码如下:


package main

import (

"fmt"

"time"

)

/*

@author: mxd

@create time: 2020/10/5

*/

func TargetFunc() {

nowSecond := time.Now().Second()

if nowSecond % 2 == 0{

fmt.Println("好")

} else {

fmt.Println("你")

}

}

然后生成so文件:


$ cd plugins

$ go build --buildmode=plugin -o TestPlugin.so TestPlugin.go

然后,不用修改任何代码,直接运行主文件

[图片上传失败...(image-98d29f-1602289163376)]

可以看到,插件已经加载成功,并被执行

致此,我们已经能够自动加载、执行插件了.

插件进阶-流程控制、原程序的方法调用

流程控制

上一进阶最后面,我们发现了一个问题,我们只能调用一个方法,而且无法控制插件的调用时机,那么我们在插件中,写入一些信息,让主体程序识别,然后在合适的时候进行调用.

首先,我们声明一个插件信息结构体,所有插件填写正确的插件信息,才能被调用

我们的主体应用流程如下:

  1. 获取当前时间

  2. 调用打印函数进行打印

  3. 分别打印

所以我们定义如下插件共享信息


package PluginTest

/*

@author: mxd

@create time: 2020/10/5

*/

// 定义主体程序流程

const (

GetTimeActive  = "get_time_active"  //获取时间的流程

DoPrintActive  = "do_print_active"  //执行打印的流程

PrintItemActive = "print_item_active" //执行分别打印

)

// 存储插件信息

type PluginBaseInfo struct {

Name        string // 插件名称

ActiveFlag  string // 插件执行的位置

ActivePoint bool  // 插件的执行点

Functions  string // 插件可用函数

// 对于可用函数,可以写为数组,来暴露更多的方法,使用一些信息标注调用时间

// 使用更细的粒度控制插件暴露的API

}

我们首先修改第一个插件信息(HelloPlugin.go)

代码如下:


package main

import (

"PluginTest"

"fmt"

"time"

)

/*

@author: mxd

@create time: 2020/10/5

*/

var PluginBaseInfo = PluginTest.PluginBaseInfo{

Name:        "hello plugin",

ActiveFlag:  PluginTest.PrintItemActive,

ActivePoint: true,          //目标任务执行前运行

Functions:  "PrintNowTime", //可用函数名

}

// 打印当前时间

func PrintNowTime() {

fmt.Println(time.Now().Second())

}

修改主体程序,使其支持插件运行控制

首先修改PluginSupport.go,使其能够获取插件更多的信息,同时添加一个方法,控制调用流程


package PluginTest

import (

"fmt"

"path"

"plugin"

)

/*

@author: mxd

@create time: 2020/10/5

*/

// PluginItem 存储着插件的信息

type PluginItem struct {

Name          string

PluginBaseInfo PluginBaseInfo

PluginItem    *plugin.Plugin

}

// 所有插件必须实现该方法

const BaseInfo = "PluginBaseInfo"

// LoadAllPlugin 将会过滤一次传入的targetFile,同时将so后缀的文件装载,并返回一个插件信息集合

func LoadAllPlugin(targetFile []string) []PluginItem {

var res []PluginItem

for _, fileItem := range targetFile {

// 过滤插件文件

if path.Ext(fileItem) == ".so" {

pluginFile, err := plugin.Open(fileItem)

if err != nil {

fmt.Println("An error occurred while load plugin : [" + fileItem + "]")

fmt.Println(err)

}

//查找指定函数或符号

targetFunc, err := pluginFile.Lookup(BaseInfo)

if err != nil {

fmt.Println("An error occurred while search target info : [" + fileItem + "]")

fmt.Println(err)

}

baseInfo, ok := targetFunc.(*PluginBaseInfo)

if !ok {

fmt.Println("Can find base info.")

}

//采集插件信息

pluginInfo := PluginItem{

Name:          fileItem,

PluginBaseInfo: *baseInfo,

PluginItem:    pluginFile,

}

res = append(res, pluginInfo)

}

}

return res

}

// DoInvokePlugin 会根据当前状态执行插件调用

func DoInvokePlugin(pluginsItems [] PluginItem, nowActive string, nowPoint bool){

for _, pluginItem := range pluginsItems{

// 判断流程

if pluginItem.PluginBaseInfo.ActiveFlag == nowActive{

// 判断执行点

if nowPoint == pluginItem.PluginBaseInfo.ActivePoint{

funcName := pluginItem.PluginBaseInfo.Functions

funcItem, err := pluginItem.PluginItem.Lookup(funcName)

if err != nil{

fmt.Println("Can't find target func in [" + pluginItem.Name +"].")

continue

}

if f, ok := funcItem.(func()); ok{

f()

}

}

}

}

}

修改主文件,添加流程定义,当然,你可以利用上下文让流程定更优雅些


package main

import (

"PluginTest"

"fmt"

"time"

)

/*

@author: mxd

@create time: 2020/10/5

*/

// 存储所有插件信息

var PluginItems []PluginTest.PluginItem

// main 主体程序入口

func main() {

// time.Now().Second 将会返回当前秒数

PluginTest.DoInvokePlugin(PluginItems, PluginTest.GetTimeActive, true)

nowSecond := time.Now().Second()

PluginTest.DoInvokePlugin(PluginItems, PluginTest.GetTimeActive, false)

PluginTest.DoInvokePlugin(PluginItems, PluginTest.DoPrintActive, true)

doPrint(nowSecond)

PluginTest.DoInvokePlugin(PluginItems, PluginTest.DoPrintActive, false)

fmt.Println("Process Stop ========")

}

// 执行打印操作

func doPrint(nowSecond int) {

PluginTest.DoInvokePlugin(PluginItems, PluginTest.PrintItemActive, true)

if nowSecond%2 == 0 {

printWorld() //偶数

} else {

printHello() //奇数

}

PluginTest.DoInvokePlugin(PluginItems, PluginTest.PrintItemActive, false)

}

// 执行打印hello

func printHello() {

fmt.Println("hello")

}

// 执行打印world

func printWorld() {

fmt.Println("world")

}

// init 函数将于 main 函数之前运行

func init() {

// 读取plugin文件夹

pluginsFiles := PluginTest.FindFile("plugins")

// 装载插件

PluginItems = PluginTest.LoadAllPlugin(pluginsFiles)

fmt.Println("Process On ==========")

}

编译HelloPlugin.go插件

运行主文件

在这里插入图片描述

调用原有程序的API

直接import原代码,然后调用即可

我们尝试利用FindFile函数,输出当前目录下的所有文件

TestPlugin.go代码:


package main

import (

"PluginTest"

"fmt"

"time"

)

/*

@author: mxd

@create time: 2020/10/5

*/

var PluginBaseInfo = PluginTest.PluginBaseInfo{

Name:        "test plugin",

ActiveFlag:  PluginTest.DoPrintActive,

ActivePoint: false,          //目标任务执行前运行

Functions:  "TargetFunc", //可用函数名

}

func TargetFunc() {

nowSecond := time.Now().Second()

if nowSecond%2 == 0 {

fmt.Println("好")

} else {

fmt.Println("你")

}

files := PluginTest.FindFile("./")

for _, fileItem := range files {

fmt.Println(fileItem)

}

}

编译、运行主文件

在这里插入图片描述

我们在此过程中,并未修改主要逻辑代码,却对行为进行了修改.

插件进阶-传参、更加优雅的调用

这里已经偏离的插件的控制了,已经涉及到模块的控制了.

对于不通的项目,已经无法依靠小幅度修改进行适配了,因此这里仅提供几种思路,不提供具体的逻辑实现.

1. 上下文

依靠程序上下文,我们可以做很多事情

我们让所有插件都接受上下文作为参数,而上下文对于插件和主体程序是共享的,因此可以依靠上下文传递变量、或者更多的信息.

日志体系完全可以依靠这种方式植入,同时,插件能够控制更多的行为和数据.

我们还可以依靠上下文控制插件能够调用的方法.

我们在每一次调用方法的时候,使用包装器或者其他手段让上下文自动更新,而上下文更新的同时去调用插件,这样,我们就和插件降耦了,而且,本身上下文也可以作为参数,提供给程序主体进行调用控制,所以我们是和上下文耦合的.

2. 参数写死

这样做的好处是,快速开发,如果我们按照方法1的方式进行开发,整个应用会变得特别臃肿:上下文、插件、流程、静态变量等众多模块将会被引入.

但是缺点也显而易见,不论是主体应用还是插件本身的维护成本很高.