前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >Go Plugin 浅析

Go Plugin 浅析

作者头像
mousemin
发布于 2023-06-10 09:45:49
发布于 2023-06-10 09:45:49
1.2K00
代码可运行
举报
文章被收录于专栏:mouseminmousemin
运行总次数:0
代码可运行

Go Plugin 浅析

go plugin 支持将 go包 编译为共享库 的形式单独发布,主程序可以在运行时动态加载这些编译为动态共享库文件的 go plugin,从中提取导出 变量 函数 的符号并在主程序的包中使用

go plugin 的这种特性为Go开发人员提供更多的灵活性,我们可以用之实现支持热插拔的插件系统。

基本使用

go官方文档明确说明 go plugin只支持Linux, FreeBSD和macOS ,这算是go plugin的第一个约束。

主程序通过 plugin 包加载 动态库 并提取 动态库文件 中的符号的过程与C语言应用运行时加载动态链接库并调用库中函数的过程如出一辙。下面我们就来看一个直观的例子。

下面是例子的结构布局

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
1└─demo01                      
2   ├─main.go          主程序       
3   ├─pkg              主程序
4   │  └─pkg.go   
5   ├─plugin           插件包
6   │  └─plugin.go    

插件代码示例

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
 1package main
 2
 3import (
 4	"fmt"
 5	"log"
 6)
 7
 8func init() {
 9	log.Println("plugin init")
10}
11
12var PluginInt int
13
14func F() {
15	fmt.Printf("plugin: public integer variable PluginInt=%d\n", PluginInt)
16}
17
18type private struct{}
19
20func (private) M1() {
21	fmt.Println("plugin: invoke private.M1")
22}

plugin包 和普通的go包 没太多区别,只是 plugin包 有一个约束:其包名必须为 main,我们使用下面命令编译该plugin:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
1go build -buildmode=plugin -o plugin.so plugin.go

如果 plugin 源代码没有放置在 main包 下面,我们在编译plugin时会遭遇如下编译器错误:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
1-buildmode=plugin requires exactly one main package

接下来,我们来看主程序

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
 1package main
 2
 3import (
 4	"log"
 5	"plugins/demo1/pkg"
 6)
 7
 8func init() {
 9	log.Println("main")
10}
11
12func main() {
13	if err := pkg.LoadPlugin("./plugin/plugin.so"); err != nil {
14		panic(err)
15	}
16}

其中 pkg/pkg.go文件内容如下

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
 1package pkg
 2
 3import (
 4	"errors"
 5	"log"
 6	"plugin"
 7)
 8
 9type MyInterface interface {
10	M1()
11}
12
13func init() {
14	log.Println("pkg init")
15}
16
17func LoadPlugin(pluginPath string) error {
18	p, err := plugin.Open(pluginPath)
19	if err != nil {
20		return err
21	}
22
23	// 导出整型变量
24	pluginInt, err := p.Lookup("PluginInt")
25	if err != nil {
26		return err
27	}
28	*pluginInt.(*int) = 15
29
30	// 导出函数变量
31	f, err := p.Lookup("F")
32	if err != nil {
33		return err
34	}
35	f.(func())()
36
37	// 导出自定义类型变量
38	f1, err := p.Lookup("Public")
39	if err != nil {
40		return err
41	}
42	i, ok := f1.(MyInterface)
43	if !ok {
44		return errors.New("f1 does not implement MyInterface")
45	}
46	i.M1()
47	return nil
48}

通过plugin 包提供的 Plugin 类型提供的 Lookup 方法在加载的动态库中查找相应的导出符号,比如上面的 PluginIntFPublic等。Lookup 方法返回plugin.Symbol 类型,而 Symbol 类型定义如下

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
1// $GOROOT/src/plugin/plugin.go
2type Symbol interface{}

我们看到 Symbol 的底层类型是interface{},因此它可以承载从 plugin 中找到的任何类型的变量函数 的符号。而 plugin 中定义的类型则是不能被主程序查找的,通常主程序也不会依赖 plugin 中定义的类型。

一旦 Lookup 成功,我们便可以将符号通过 类型断言 获取到其真实类型的实例,通过这些 实例 (变量函数),我们可以调用 plugin 中实现的逻辑。编译plugin 后,运行上述主程序,我们可以看到如下结果:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
12023/04/03 14:14:48 pkg init
22023/04/03 14:14:48 main
32023/04/03 14:14:48 plugin init
4plugin: public integer variable PluginInt=15
5plugin: invoke private.M1

主程序是如何知道导出的符号究竟是函数还是变量呢? 取决于主程序插件系统的设计,因为主程序与plugin间必然要有着某种 契约约定。 就像上面主程序定义的 MyInterface 接口类型,它就是一个主程序与plugin之间的约定,plugin中只要暴露实现了该接口的类型实例,主程序便可以通过MyInterface 接口类型实例与其建立关联并调用 plugin 中的实现 。

包的初始化

上面的例子中我们看到,插件的初始化发生在主程序 open 动态库文件时。

按照官方文档的说法:“当一个插件第一次被open时,plugin中所有不属于主程序的包的init函数将被调用,但一个插件只被初始化一次,而且不能被关闭”。

我们来验证一下在主程序中多次加载同一个 plugin 的情况

其中 main.go 修改为

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
 1package main
 2
 3import (
 4	"log"
 5	"plugins/demo1/pkg"
 6)
 7
 8func init() {
 9	log.Println("main")
10}
11
12func main() {
13	if err := pkg.LoadPlugin("./plugin/plugin.so"); err != nil {
14		panic(err)
15	}
16	log.Println("LoadPlugin ok")
17
18	if err := pkg.LoadPlugin("./plugin/plugin.so"); err != nil {
19		panic(err)
20	}
21	log.Println("ReLoadPlugin ok")
22}

pkg/pkg.go添加包的依赖

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
1package main
2
3import (
4	"fmt"
5	"log"
6
7	_ "plugins/demo1/pkg"
8)
9// ....

运行上述代码:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
12023/04/03 14:17:46 pkg init
22023/04/03 14:17:46 main
32023/04/03 14:17:46 plugin init
4plugin: public integer variable PluginInt=15
5plugin: invoke private.M1
62023/04/03 14:17:46 LoadPlugin ok
7plugin: public integer variable PluginInt=15
8plugin: invoke private.M1
92023/04/03 14:17:46 ReLoadPlugin ok

通过这个输出结果,我们验证了两点说法:

  • 重复加载同一个plugin,不会触发多次plugin包的初始化,上述结果中仅输出一次:`plugin init
  • plugin中依赖的包,但主程序中没有的包,在加载plugin时,这些包会被初始化,如:pkg init

使用约束

go plugin 应用不甚广泛的一个主因是其约束较多,这里我们来看一下究竟 go plugin 都有哪些约束

  • 主程序与plugin的共同依赖包的版本必须一致
  • 如果采用mod=vendor构建,那么主程序和plugin必须基于同一个vendor目录构建
  • 主程序与plugin使用的编译器版本必须一致
  • 使用plugin的主程序仅能使用动态链接

版本管理

使用动态链接实现插件系统,一个更大的问题就是插件的版本管理问题。

linux上的动态链接库采用soname的方式进行版本管理。soname的关键功能是它提供了兼容性的标准,当要升级系统中的一个库时,并且新库的soname和老库的soname一样,用旧库链接生成的程序使用新库依然能正常运行。这个特性使得在Linux下,升级使得共享库的程序和定位错误变得十分容易。

什么是soname呢?在 /lib/usr/lib 等集中放置共享库的目录下,你总是会看到诸如下面的情况:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
1lrwxrwxrwx 1 root root    19 1115 2021 /usr/lib64/libXxf86vm.so -> libXxf86vm.so.1.0.0
2lrwxrwxrwx 1 root root    19 618 2021 /usr/lib64/libXxf86vm.so.1 -> libXxf86vm.so.1.0.0
3-rwxr-xr-x 1 root root 23696 82 2017 /usr/lib64/libXxf86vm.so.1.0.0

共享库的惯例中每个共享库都有多个名字属性,包括real namesonamelinker name

  • real name 指的是实际包含共享库代码的那个文件的名字(如上面例子中的 libXxf86vm.so.1.0.0),也是在共享库编译命令行中-o后面的那个参数
  • sonameshared object name 的缩写,也是这三个名字中最重要的一个,无论是在编译阶段还是在运行阶段,系统链接器都是通过共享库的 soname (如上面例子中的libXxf86vm.so.1)来唯一识别共享库的。 即使real name相同但soname不同,也会被链接器认为是两个不同的库。
  • linker name是编译阶段提供给编译器的名字(如上面例子中的libXxf86vm)。如果你构建的共享库的real name是类似于上例中libXxf86vm.so.1.0.0 那样的带有版本号的样子,那么你在编译器命令中直接使用-L path -lXxf86vm是无法让链接器找到对应的共享库文件的,除非你为 libXxf86vm.so.1.0.0 提供了一个linker namelinker name一般在共享库安装时手工创建。
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2023年04月03日,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
golang plugin源码分析
Golang是静态编译型语言,在编译时就将所有引用的包(库)全部加载打包到最终的可执行程序(或库文件)中,因此并不能在运行时动态加载其他共享库。Go Plugin提供了这样一种方式,能够让你在运行时动态加载外部功能。go在1.8 支持了这个功能,类似c语言的动态链接库。
golangLeetcode
2022/08/02
1K0
go: 官方插件(plugin)初探
总体来说,plugin 包还有相当大的提升空间,但这似乎并不是go团队的关注重点。
超级大猪
2024/01/17
1580
go微服务系列之三
在前两篇系列博文中,我已经实现了user-srv、web-srv、api-srv,在新的一篇博文中,我要讲解的是如何在项目中如何使用redis存储session。如果想直接查阅源码或者通过demo学习的,可以访问ricoder_demo。
李海彬
2019/05/08
7390
go微服务系列之三
Go 1.8 相比 Go 1.7 有哪些值得注意的改动?
Go 1.8 引入了一个语言规范上的变化:在进行显式的结构体类型转换时,编译器将不再考虑结构体字段的标签 (tags)。这意味着,如果两个结构体类型仅仅是字段标签不同,而字段的名称、类型和顺序完全相同,那么它们之间可以进行直接的类型转换。
Piper破壳
2025/04/18
320
一起用golang之Go程序的套路
系统性地介绍golang基础的资料实在太多了,这里不再一一赘述。本文的思路是从另一个角度来由浅入深地探究下Go程序的套路。毕竟纸上得来终觉浅,所以,能动手就不要动口。有时候几天不写代码,突然间有一天投入进来做个东西,才恍然发觉,也只有敲代码的时候,才能找回迷失的自己,那可以忘掉一切的不开心。
李海彬
2018/09/29
9570
一起用golang之Go程序的套路
在 Golang 项目中使用 Spring Cloud Config Server 管理配置
最近用 Go 写后端写得很开心,写篇比较实用的博客总结下如何通过 Spring Cloud Config Server 管理 Go 程序中的配置。 实现并不复杂,因此也可以很轻易地推广到其他语言的程序中。
李海彬
2018/07/26
2K0
在 Golang 项目中使用 Spring Cloud Config Server 管理配置
golang新手容易犯的3个错误
这是因为代码中只是声明了map的类型,却没有为map创建底层数组,此时的map实际上在内存中还不存在,即nil map,可以运行下面的代码进行验证:
李海彬
2018/09/29
1.4K0
golang新手容易犯的3个错误
Go内嵌静态资源
最常见的,比如一个混编网址的后端程序,本来需要把程序与它所需要的静态资源(html模版、css、js、图片)一起上传至生产服务器,同时还需要正确配置静态资源在服务器中的路径让程序能正常访问.现在我们将这些资源全部嵌入到程序中,部署的时候只需要部署一个二进制文件,配置也只针对这个程序本身,部署的流程大大简化.
mousemin
2023/06/10
7690
Go内嵌静态资源
Golang工程经验(上)
作为一个C/C++的开发者而言,开启Golang语言开发之路是很容易的,从语法、语义上的理解到工程开发,都能够快速熟悉起来;相比C、C++,Golang语言更简洁,更容易写出高并发的服务后台系统
李海彬
2018/10/08
2K1
Golang工程经验(上)
一日一学_Go语言mgo(mongo场景应用)
注意: 上图已经告知我们mongo不支持事务,在开发项目应用时,想要保证数据的完整性请考虑关系型数据库(经典例子银行转账)。 mongo提供了许多原子操作,比如文档的保存,修改,删除等,都是原子操作。所谓原子操作就是要么这个文档保存到mongodb,要么没有保存到mongodb,不会出现查询到的文档不完整的情况。
李海彬
2019/01/08
1.4K0
Golang并发的次优选择:sync包
我们都知道Golang并发优选channel,但channel不是万能的,Golang为我们提供了另一种选择:sync。通过这篇文章,你会了解sync包最基础、最常用的方法,至于sync和channel之争留给下一篇文章。
大彬
2019/04/11
6250
微服务系列笔记之RabbitMQ的入门使用
上一篇文章我们讲到了broker模式,其实在Micro框架中已经为我们提供了一个rabbitMQ插件,我们可以借助这个插件来实现我们的生产与消费
陌无崖
2019/08/16
5430
微服务系列笔记之RabbitMQ的入门使用
Go 插件功能的实现方式
golang 1.8 及以上版本提供了一个创建共享库(shared object)的新工具,称为 Plugins。目前 Plugins 仅在 Linux、FreeBSD 和 macOS 上受支持,且只支持 golang 调用。
gopher云原生
2021/10/18
1.2K0
原创投稿--数据可视化库go-echarts
我是一名 Golang 开发爱好者 今天开源了一个数据可视化库 想填补一下 Golang 在这方面的空缺 项目地址是:https://github.com/chenjiandongx/go-echarts
李海彬
2019/05/22
1.6K0
原创投稿--数据可视化库go-echarts
Go 2.0发布在即,程序员有太多话要说
Go语言的开发者正着手准备开发2.0版本,并从以下三个方面发布了初步的设计方案(非官方正式版),以供社区开展讨论:
新智元
2018/09/25
2.4K0
Go 2.0发布在即,程序员有太多话要说
NVIDIA/k8s-device-plugin源码分析
Author: xidianwangtao@gmail.com k8s-device-plugin内部实现原理图 在Kubernetes如何通过Device Plugins来使用NVIDIA GP
Walton
2018/04/18
4.2K2
NVIDIA/k8s-device-plugin源码分析
写在学习golang一个月后
连接池。由于PHP没有连接池,当高并发时就会有大量的数据库连接直接冲击到MySQL上,最终导致数据库挂掉。虽然Swoole有连接池,但是Swoole只是PHP的一个扩展,之前使用Swoole过程中就踩过很多的坑。经过我们的讨论还是觉得使用Golang更加可控一些。
李海彬
2018/10/08
1.1K0
写在学习golang一个月后
Golang: 插件plugin介绍
同时官方文档也提示了:Currently plugins are only supported on Linux and macOS .它目前支持Linux和Mac操作系统(不支持windows)
OwenZhang
2021/12/08
1.8K0
golang cron 定时任务
最开始接触定时任务的概念是在大二的一个计算机操作系统设计的实验课上,当时老师给了五个任务要求,自己任选三个小组完成。
李海彬
2019/05/08
12.3K0
Golang学习笔记之日志log、zap
(1)Golang's log模块主要提供了3类接口。分别是 “Print 、Panic 、Fatal ”,对每一类接口其提供了3中调用方式,分别是 "Xxxx 、Xxxxln 、Xxxxf",基本和fmt中的相关函数类似。
李海彬
2018/12/29
2.3K0
相关推荐
golang plugin源码分析
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验