• golang 动态库 (buildmode)


    1. golang 动态库

    2. golang 语言使用动态库、调用动态链接库

    2.1. Go 插件系统

    通过使用插件在运行时扩展程序的功能,而无需重新编译程序,这是一个很常见的功能需求,特别是在模块化设计的程序里面,比如 Nginx 的模块系统。 在 C/C++中通过使用动态库的方式可以实现动态加载,但是 Go 直到 1.8 官方才开始支持,下面将介绍 Go 如何基于动态链接库来实现动态加载。

    2.2. 动态加载的优劣

    优点:

    • 动态加载,也称热加载,每次升级时不用重新编译整个工程,重新部署服务,而是添加插件时进行动态更新。这对于很多比较重型的服务来说非常重要。

    缺点:

    • 带来一定的安全风险,如果一些非法模块被注入如何防范
    • 给系统带来一定的不稳定的因素,如果模块有问题,没有经过良好的测试,容易导致服务崩溃
    • 为版本管理带来了难题,特别是在微服务的今天,同一个服务,加载了不同的插件,应该怎么管理版本,插件版本应该如何管理

    因此请慎重考虑,是使用动态插件还是在源码里面进行插件化。

    2.3. Go 的插件系统:Plugin

    从 1.8 版开始,官方提供了这种插件化的手段:plugin. 此功能使程序员可以使用动态链接库构建松散耦合的模块化程序,可以在运行时动态加载和绑定。

    Go 插件是使用 -buildmode = plugin 标记编译的一个包,用于生成一个共享对象 (.so) 库文件。 Go 包中的导出的函数和变量被公开为 ELF 符号,可以使用 plugin 包在运行时查找并绑定 ELF 符号。Go 编译器能够使用 build flag -buildmode = c-shared 创建 C 风格的动态共享库。

    1.8 版本插件功能只能在 Linux 上使用。 1.10 也可以在 Mac 上运行。

    下面将介绍使用 Go 插件系统创建模块化软件的一些开发原则,并提供一个功能齐全的示例。

    2.4. 插件开发原则

    使用 Go 插件创建模块化程序需要遵循与常规 Go 软件包一样严格的软件实践。然而,插件引入了新的设计问题,因为它们的解耦性质被放大了。因此我们在设计可插拔系统时,有一些原则需要关注:

    2.4.1. 插件独立

    应该将插件视为与其他组件分离的独立组件。这允许插件独立于他们的消费者,并拥有自己的开发和部署生命周期。注意插件的可用性很重要,因为它有肯能为整个系统带来不稳定的因素,因此系统必须为插件集成提供一个简单的封装层,插件开发人员将系统视为黑盒,不作为所提供的合约以外的假设,从而保证插件自身的可用性。

    2.4.2. 使用接口类型作为边界

    Go 插件可以导出任何类型的包函数和变量。您可以设计插件来将其功能解耦为一组松散的函数。缺点是您必须单独查找和绑定每个函数符号。
    然而,更为简单的方法是使用接口类型。创建导出功能的接口提供了统一简洁的交互,并具有清晰的功能划分。解析到接口的符号将提供对该功能的整个方法集的访问,而不仅仅是一个方法。

    2.4.3. Unix 模块化原则

    插件代码应该设计成只关注一个功能点。

    2.4.4. 版本控制

    插件是不透明而独立的实体,应该进行版本控制,以向用户提示其支持的功能。这里的一个建议是在命名共享对象文件时使用语义版本控制。例如,上面的文件编译插件可以命名为 eng.so.1.0.0。

    2.5. 插件开发示例

    我以我遇到的一个实际需求为例,在开发物联网接入组件的时候,需要动态支持物解析,下面就开发一个物解析的插件系统。

    下面是项目结构,parser.go 是接口规约,main.go 是主程序,plugins 存放多个插件包

    ├── main.go
    ├── parser.go
    └── plugins
        ├── car
        │   └── car.go
        └── phone
            └── phone.go
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    2.5.1. 编写插件

    • 编写主程序接口规约:main.go
    package main
     
    // Parser use to parse things
    type Parser interface {
    byte) (meta map[string]string, data map[string]float64, err error)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 根据接口规约编写插件:car.go
    package main
     
    type car string
     
    func (c *car) Parse([]byte) (meta map[string]string, data map[string]float64, err error) {
    map[string]string{"key1": "a"}
    map[string]float64{"key1": 1}
     
    return meta, data, nil
    }
     
    var Car car
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 根据接口规约编写插件:phone.go
    package main
     
    type phone string
     
    func (p *phone) Parse([]byte) (meta map[string]string, data map[string]float64, err error) {
    map[string]string{"key1": "b"}
    map[string]float64{"key1": 2}
     
    return meta, data, nil
    }
     
    var Phone phone
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 编译插件插件写完后将在 plugins 目录下编译插件:
    $ cd plugins
    $ go build -buildmode=plugin -o car.so car/car.go
    $ go build -buildmode=plugin -o phone.so phone/phone.go
    
    • 1
    • 2
    • 3

    最终在 plugins 目录下会生成好我们编译好的插件:

    $ ls *.so
    car.so   phone.so
    
    • 1
    • 2

    2.5.2. 使用插件

    插件的使用很简单,大概步骤如下:

    • 用 plugin.Open() 打开插件文件
    • 用 plguin.Lookup(“Export-Variable-Name”) 查找导出的符号”Car”或者”Phone”。 请注意,符号名称与插件模块中定义的变量名称相匹配
    • 使用该变量

    主程序使用插件:main.go

    package main
     
    import (
    "fmt"
    "plugin"
    )
     
    // Parser use to parse things
    type Parser interface {
    byte) (meta map[string]string, data map[string]float64, err error)
    }
     
    func pa() {
    "./plugins/car.so")
    if err != nil {
    panic(err)
    	}
     
    "Car")
    if err != nil {
    panic(err)
    	}
     
    	p, ok := car.(Parser)
    if ok {
    byte("a"))
    if err != nil {
    panic(err)
    		}
    "meta: %v, data: %v \n", meta, data)
    	}
    }
     
    func pb() {
    "./plugins/phone.so")
    if err != nil {
    panic(err)
    	}
     
    "Phone")
    if err != nil {
    panic(err)
    	}
     
    	p, ok := phone.(Parser)
    if ok {
    byte("a"))
    "meta: %v, data: %v \n", meta, data)
    	}
    }
     
    func main() {
    	pa()
    	pb()
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55

    测试是否正常运行:

    $ go run main.go
    meta: map[key1:a], data: map[key1:1]
    meta: map[key1:b], data: map[key1:2]
    
    • 1
    • 2
    • 3
  • 相关阅读:
    【牛客 - 剑指offer】JZ50 第一个只出现一次的字符 Java实现
    KLEE简单使用
    java spring cloud 工程企业管理软件-综合型项目管理软件-工程系统源码
    微服务(SpringCloud)使用汇总
    成功解决ES高亮内容引起的字段显示不一致问题
    启发式的搜索策略
    离线打包maven设置
    【算法】搜索大法
    vector类模拟实现(c++)(学习笔记)
    澳洲猫罐头到底怎么样呢?我自己亲自喂养过的优质猫罐头分享
  • 原文地址:https://blog.csdn.net/wan212000/article/details/134451021