Golang 插件化开发

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

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

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

文章目录

  • Golang 插件化开发
    • 基本思路
      • 具有模拟业务的主体程序
        • 主体代码
      • 简单的插件
        • 插件代码
        • 修改的主体代码
    • 插件进阶-批量化
      • 批量化
        • 自动读取文件夹下的插件
          • MainFile更新代码如下
        • 插件装载
          • 插件支持代码如下
        • 增加插件进行测试
    • 插件进阶-流程控制、原程序的方法调用
      • 流程控制
      • 调用原有程序的API
    • 插件进阶-传参、更加优雅的调用
      • 1. 上下文
      • 2. 参数写死

基本思路

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

具有模拟业务的主体程序

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

  1. 当时间秒数为奇数的时候,输出hello
  2. 当时间秒数为偶数的时候,输出world
主体代码

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

主文件名:MainFile.go
package mainimport ("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 ==========")
}

输出如下:

简单的插件

然后我们编写一个插件代码
插件代码的入口package也要为main,但是可以不包含main方法

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

插件文件名:HelloPlugin.go
插件代码
package mainimport ("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 mainimport ("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 PluginTestimport ("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 []stringfor _, 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 mainimport ("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 PluginTestimport ("fmt""path""plugin"
)/*
@author: mxd
@create time: 2020/10/5
*/// PluginItem 存储着插件的信息
type PluginItem struct {Name       stringTargetFunc plugin.Symbol
}// 所有插件必须实现该方法
const TargetFuncName = "TargetFunc"// LoadAllPlugin 将会过滤一次传入的targetFile,同时将so后缀的文件装载,并返回一个插件信息集合
func LoadAllPlugin(targetFile []string) []PluginItem {var res []PluginItemfor _, 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 mainimport ("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 mainimport ("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

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

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

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

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

流程控制

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

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

  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 mainimport ("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 PluginTestimport ("fmt""path""plugin"
)/*
@author: mxd
@create time: 2020/10/5
*/// PluginItem 存储着插件的信息
type PluginItem struct {Name           stringPluginBaseInfo PluginBaseInfoPluginItem     *plugin.Plugin
}// 所有插件必须实现该方法
const BaseInfo = "PluginBaseInfo"// LoadAllPlugin 将会过滤一次传入的targetFile,同时将so后缀的文件装载,并返回一个插件信息集合
func LoadAllPlugin(targetFile []string) []PluginItem {var res []PluginItemfor _, 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.FunctionsfuncItem, 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 mainimport ("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 mainimport ("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的方式进行开发,整个应用会变得特别臃肿:上下文、插件、流程、静态变量等众多模块将会被引入.
但是缺点也显而易见,不论是主体应用还是插件本身的维护成本很高.

GoLang 插件化开发相关推荐

  1. TinyFrame升级之八:实现简易插件化开发

    本章主要讲解如何为框架新增插件化开发功能. 在.net 4.0中,我们可以在Application开始之前,通过PreApplicationStartMethod方法加载所需要的任何东西.那么今天我们 ...

  2. Android插件化开发之解决OpenAtlas组件在宿主的注冊问题

    Android插件化开发之解决OpenAtlas组件在宿主的注冊问题 OpenAtlas有一个问题,就是四大组件必须在Manifest文件里进行注冊,那么就必定带来一个问题,插件中的组件都要反复在宿主 ...

  3. Android插件化开发之动态加载三个关键问题详解

    本文摘选自任玉刚著<Android开发艺术探索>,介绍了Android插件化技术的原理和三个关键问题,并给出了作者自己发起的开源插件化框架. 动态加载技术(也叫插件化技术)在技术驱动型的公 ...

  4. Android插件化开发指南——Hook技术(一)【长文】

    文章目录 1. 前言 2. 将外部dex加载到宿主app的dexElements中 3. 插件中四大组件的调用思路 4. Hook 2.1 对startActivity进行Hook 2.1.1 AMS ...

  5. Python为什么要使用包管理、插件化开发?

    一.包管理 1.为什么使用包管理 目的是为了便于共享.为了更多项目调用使用,或者共享给别人,就需要打包,目的是为了复用. Pypi(Python Package Index)公共的模块存储中心.htt ...

  6. android中使用tmf框架插件化开发的问题

    android中使用tmf框架插件化开发的问题 最近项目开发使用的是tmf框架,其中大多数都是通过源生和H5交互的方式来实现的,大体实现和别的三方框架是一样的,需要按照tmf的官方文档引入一些lib和 ...

  7. Android插件化开发之动态加载本地皮肤包进行换肤

    Android插件化开发之动态加载本地皮肤包进行换肤 前言: 本文主要讲解如何用开源换肤框架 android-skin-loader-lib来实现加载本地皮肤包文件进行换肤,具体可自行参考框架原理进行 ...

  8. Android插件化开发指南——插件化技术简介

    文章目录 1. 为什么需要插件化技术 2. 插件化技术的历史 3. 插件化实现思路 3.1 InfoQ:您在 GMTC 中的议题叫做<Android 插件化:从入门到放弃>,请问这个标题代 ...

  9. php 插件化开发模式,JavaScript_JavaScript插件化开发教程(六),一,开篇分析 今天这篇文章 - phpStudy...

    JavaScript插件化开发教程(六) 一,开篇分析 今天这篇文章我们说点什么那?嘿嘿嘿.我们接着上篇文章对不足的地方进行重构,以深入浅出的方式来逐步分析,让大家有一个循序渐进提高的过程.废话少说, ...

  10. Android插件化开发指南——实践之仿酷狗音乐首页

    文章目录 1. 前言 2. 布局分析 3. 底部导航栏的实现 4. 顶部导航栏和ViewPager+Fragment的关联 1. 前言 在Android插件化开发指南--2.15 实现一个音乐播放器A ...

最新文章

  1. 在苏州的一个超级棒的事情
  2. HDU 6155 Subsequence Count(矩阵乘法+线段树+基础DP)
  3. 【jmx】java jmx 获取 kafka topic的logStart LogEnd信息
  4. IE6 透明遮挡falsh解决方案
  5. 怎样区分直连串口线和交叉串口线?
  6. [Android Security] DEX文件格式分析
  7. modelsim 下载链接
  8. 设计模式23篇(VIP典藏版)
  9. 中国石油大学《化工原理二》第一阶段在线作业
  10. idea 行号栏太宽以及显示一些图标问题解决
  11. cox回归模型python实现_生存分析Cox回归模型(比例风险模型)的spss操作实例
  12. 中国计算机学会推荐学术会议/期刊(网络与信息安全部分)
  13. 深入浅出AT命令(5)-短信命令
  14. e-mobile服务器地址显示无法登陆,E-Mobile服务器安装设置手册.doc
  15. 790-C语言的数组元素下标为何从0开始?
  16. 忘记密码怎么启动计算机,电脑忘记密码如何重装系统?
  17. [算法学习]模拟退火算法(SA)、遗传算法(GA)、布谷鸟算法(CS)、人工蜂群算法(ABC)学习笔记---附MATLAB注释代码
  18. mac上 go build的二进制文件在Linux上运行提示cannot execute binary file或者-bash: ./sayHello: 无法执行二进制文件的解决方式
  19. WakeOnLAN(WOL)测试
  20. C# .CS后台调用JS函数

热门文章

  1. java 性能优化:35 个小细节,让你提升 java 代码的运行效率
  2. 音频处理之语音加速播放
  3. 计算机网络图标显示不出来,网络图标不见了汇总解决教程
  4. SAP FICO 固定资产会计 功能详解
  5. JBPM工作流框架应用
  6. Windows Server 2003 R2标准版 SP2 64位 (简体中文)官方原版ISO镜像
  7. perl语言入门:子程序
  8. MapXtreme绿色部署
  9. C# MapXTreme移动点与画线的简单方法记录
  10. JDBC Java数据库编程