Go插件:轻松实现模块化开发与动态功能扩展

1,210 阅读5分钟

简介

Go 语言(通常称为 Golang )是一种静态类型、编译型语言。

golang 并不像动态语言一样,修改了代码后就可以直接生效。

他必须编译后才能使新代码功能生效。

这就导致某些特定的场合不适用。比如 tcp 协议动态解析。

go1.8 版本开始提供了一个创建共享库的新工具:plugins

golang plugins

Go 语言的插件机制允许开发者在运行时动态加载和执行 Go 编译的插件。

Go1.8 版本开始引入了对插件的支持,使用go build -buildmode=plugin 可以创建一个插件,这通常是一个包含可导出函数和变量的Go主包编译成的共享库文件 。

插件在首次打开时会调用其所有包的初始化函数,但不运行 main 函数,并且一旦初始化完成,插件不能被关闭.

Go插件的优势包括:

1.动态扩展性:可以在不重启应用的情况下添加或更新功能,对于需要持续运行的服务来说非常重要。

2.模块化开发:鼓励将复杂应用分解为小的、独立维护的组件,提高了代码的可维护性和复用性。

3.版本隔离:不同插件间的依赖关系可以独立管理,降低了版本冲突的风险。

4.安全与沙盒:插件在单独的 goroutine 中运行,提供了一定程度的隔离。

案例:创建一个tcp协议的动态解析服务

创建一个plugin_code目录,并创建插件

plugin.go

// +build plugin
package main

import "fmt"

func main()  {
}


// 导出的函数,处理函数
func Handle(res any)(any,error) {
	//此处填写你的数据处理逻辑
	fmt.Println("res data:",res)

	return res,nil

}


此插件,接收一个参数,返回处理后的数据和错误

创建server目录,并编写各种服务

  1. 创建一个 server_factory.go ,写入以下代码
package server

import (
	"errors"
)

// 获取实例
type ServerFactory interface {
	GetInstance(config any) (ServerInterface, error)
}

// 监听服务
type ServerInterface interface {
	InitServer()
}


// 获取实例
func GetInstance(config map[string]any) (*ServerInterface, error) {

	var server ServerInterface
	var err error
	protocol, ok := config["protocol"]
	if !ok {
		return nil, errors.New("config protocol is not find")
	}
	protocolString,ok := protocol.(string)
	if !ok {
		return nil,errors.New("config protocol type is error")
	}

	switch protocolString {
	case "TCP":
		server, err = new(TcpFactory).GetInstance(config)
		break
	case "UDP":
		break
	}

	return &server, err
}

  1. 创建一个 tcp.go ,写入以下代码
package server

import (
	"errors"
	"fmt"
	"net"
	"path/filepath"
	"plugin"
	"strings"
)

type TcpServer struct {
	Port int //端口号
	Tls bool //是否开启tls
	BuffLen int //数据长度
	Server *net.Listener
	FuncHandle func(res any)(any,error) //数据解析函数
	ConfigId int //配置数据id
	PluginPath string //插件路径
}

type TcpFactory struct {

}

/**
	初始化tcp server
	config 配置参数
 */
func (tcpfactory *TcpFactory) GetInstance(config map[string]any) (ServerInterface,error) {

	pluginPath, ok := config["plugin_path"]
	if !ok {
		return nil,errors.New("config plugin_path is not find")
	}
	id, ok := config["id"]
	if !ok {
		return nil,errors.New("config id is not find")
	}
	port, ok := config["port"]
	if !ok {
		return nil,errors.New("config port is not find")
	}

	configId,ok := id.(int)
	if !ok {
		return nil,errors.New("config id type is error")
	}

	newPort,ok := port.(int)
	if !ok {
		return nil,errors.New("config port type is error")
	}

	//获取路径
	newPath,ok := pluginPath.(string)
	if !ok {
		return nil,errors.New("config plugin_path type is error")
	}

	filename := filepath.Base(newPath)
	newName:=strings.Split(filename, ".")[0]
	soPath :=filepath.Join("./network/plugin/"+newName+".so")

	//初始化数据解析函数
	// 加载插件
	p, err := plugin.Open(soPath)
	if err != nil {
		return nil,err
	}
	// 查找插件中的函数,Handle
	f, err := p.Lookup("Handle")
	if err != nil {
		fmt.Println(err)
		return nil,err
	}
	// 转换为对应的函数类型并调用
	helloFunc := f.(func(res any)(any,error))

	return &TcpServer{BuffLen: 1024,
		FuncHandle:helloFunc,
		ConfigId: configId,
		PluginPath:soPath,
		Port: newPort},nil
}

//监听服务,并加载插件处理数据
func (tcp *TcpServer) InitServer()  {
	// 监听TCP端口
	address := fmt.Sprintf("0.0.0.0:%d",tcp.Port)
	listener, err := net.Listen("tcp", address)
	if err != nil {
		fmt.Println("Error listening:", err)
		return
	}
	tcp.Server = &listener
	defer (*tcp.Server).Close()
	//defer listener.Close()

	for {
		conn, err := (*tcp.Server).Accept()
		if err != nil {
			fmt.Println("Error accepting:", err)
			continue
		}
        //数据处理
		go tcp.HandleData(conn)
	}
}

//数据处理
func (tcp *TcpServer) HandleData(conn net.Conn)  {
	defer conn.Close()
	// 读取数据
	buffer := make([]byte, tcp.BuffLen)
	n, err := conn.Read(buffer)
	if err != nil {
		fmt.Println("Error reading:", err)
		return
	}
	//数据交给插件动态解析
	resData,eror:=tcp.FuncHandle(buffer[:n])
	if eror !=nil{
		fmt.Println("Error reading:", err)
		return
	}
	//TODO:: 数据流转
	fmt.Println("Tcp port:",tcp.Port,"resData:",resData)
}

创建一个plugin目录,存放编译后的插件

此时目录结构如下:

安装

编写main.go 写入以下内容测试:

package main

import (
	server2 "bimcc-gin-api/network/server"
	"fmt"
	"os/exec"
	"path/filepath"
	"strings"
)

func main() {

	test()
}


func test() {

	path:="network/plugin_code/1722840336825146228-0957b95926b67b013882.go"
	config := make(map[string]interface{})
	config["protocol"] = "TCP"
	config["plugin_path"] = path
	config["id"] = 0
	config["port"] = 12000
	//编译插件
	filename := filepath.Base(path)
	//fmt.Println("Filename:", filename)
	newName:=strings.Split(filename, ".")[0]
	//go build -buildmode=plugin -o plugin_example.so plugin_example.go
	out:=filepath.Join("./network/plugin/"+newName+".so")
	inDir:=filepath.Join("./"+path)
	fmt.Println("out:",out)
	fmt.Println("inDir:",inDir)
	cmd := exec.Command("go", "build","-buildmode","plugin", "-o", out, inDir)
	if err := cmd.Run(); err != nil {
		fmt.Println("so error:",err.Error())
		return
	}

	//监听服务,并利用插件动态解析tcp数据
	server, er := server2.GetInstance(config)
	if er != nil {
		fmt.Println("error:", er)
		return
	}
	(*server).InitServer()
	return
}


在linux服务器上面运行以下代码:

go run main.go

注意:plugins 目前仅支持Linux和macOS系统,不支持Windows

安装

此时已经监听了 tcp 端口。

我们查看 network/plugin 目录是否生成了插件文件

安装

此时插件已经编译生成了。

我们可以用 tcp 工具测试连接测试一下,看是否进入到我们的插件函数里面。

安装

此时查看控制台是否收到数据:

安装

证明已经收到数据,进入了插件函数,并且函数处理后也返回来了。至此功能已经验证完成。

总结

Go 插件的应用场景包括微服务架构、功能插件化、开发工具和 IDE 以及云原生应用 。

然而,Go 插件也有一些限制和不足,例如目前仅支持 LinuxmacOS 系统,不支持 Windows

此外,使用插件可能会增加程序的复杂性,需要仔细考虑设计,并且插件和主应用程序必须使用完全相同的Go工具链版本构建。

– 欢迎点赞、关注、转发、收藏【我码玄黄】,各大平台同名。