简介
Go 语言(通常称为 Golang )是一种静态类型、编译型语言。
golang 并不像动态语言一样,修改了代码后就可以直接生效。
他必须编译后才能使新代码功能生效。
这就导致某些特定的场合不适用。比如 tcp 协议动态解析。
go1.8 版本开始提供了一个创建共享库的新工具:plugins
golang plugins
Go 语言的插件机制允许开发者在运行时动态加载和执行 Go 编译的插件。
Go 从 1.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目录,并编写各种服务
- 创建一个
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
}
- 创建一个
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 插件也有一些限制和不足,例如目前仅支持 Linux 和 macOS 系统,不支持 Windows 。
此外,使用插件可能会增加程序的复杂性,需要仔细考虑设计,并且插件和主应用程序必须使用完全相同的Go工具链版本构建。
– 欢迎点赞、关注、转发、收藏【我码玄黄】,各大平台同名。