Go基于plugin的热更新初体验

背景

对于一个部署在生产环境的项目来说,我们希望当代码出现bug的时候,可以不用重启进程而达到动态修改代码的目的------

这就是代码热部署

使用java做游戏服务器,最大的好处是,当代码出现bug,可以直接热更新代码来解决,而无须重启服务器。

如果使用JVM的Instrumentation功能,可以实现方法体内部的代码热更新,具体原理及操作可参考

游戏服务端框架之代码热部署(一)

如果使用类单列替换,甚至可以实现在类内部添加新的属性或者方法,具体原理及操作可参考

游戏服务端框架之代码热部署(二)

Go热更新

基本演示

插件代码 plugin.go

Go 复制代码
package main

import "fmt"

func SayHello() {
    fmt.Println("11111")
}

编译插件

在 Windows 命令行中,使用以下命令编译插件:

bash 复制代码
go build -buildmode=plugin -o plugin.dll plugin.go

遗憾的是,截止到go 1.23.0,windows暂不支持plugin模式,直接报错:

go build -buildmode=plugin -o plugin.dll plugin.go

-buildmode=plugin not supported on windows/amd64

改成linux测试

go build -o plugin.so -buildmode=plugin plugin.go

主程序代码 main.go

Go 复制代码
package main

import (
    "fmt"
    "plugin"
    "time"
)

func loadPlugin() (func(), error) {
    p, err := plugin.Open("plugin.so") 
    if err != nil {
        return nil, err
    }

    sayHello, err := p.Lookup("SayHello")
    if err != nil {
        return nil, err
    }

    return sayHello.(func()), nil
}

func main() {
    sayHello, err := loadPlugin()
    if err != nil {
        fmt.Println("Error loading plugin:", err)
        return
    }
    sayHello()

    // 模拟文件监控,这里简单使用定时检查
    ticker := time.NewTicker(5 * time.Second)
    for range ticker.C {
        newSayHello, err := loadPlugin()
        if err == nil {
            sayHello = newSayHello
            fmt.Println("Plugin reloaded.")
        }
        sayHello()
    }
}

修改plugin.go代码

Go 复制代码
package main

import "fmt"

func SayHello() {
    fmt.Println("2222")
}

重新编译,发现重新加载了插件,但打印还是旧的。 百思不得其解,尝试添加输出文件的修改日期,或者输出函数指针地址,都找不到原因。最后,在网上偶然看到有文章说,plugin.Open()函数,对于同一个文件名称,只会加载一次。

由此想到一种思路,每次编译使用不同的名称,然后通过http的方式,通过main函数加载新的插件名称。代码如下:

Go 复制代码
func updatePluginName(c *gin.Context) {
    pluginName := c.Query("name")
    if pluginName == "" {
        c.JSON(http.StatusBadRequest, gin.H{"error": "Plugin name is required"})
        return
    }

    newSayHello, err := loadPlugin(pluginName)
    if err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to load plugin: %v", err)})
        return
    }

    sayHello = newSayHello
    c.JSON(http.StatusOK, gin.H{"message": "Plugin reloaded successfully"})
}

func main() {
    r := gin.Default()

    // 定义更新插件文件名的接口
    r.GET("/update-plugin", updatePluginName)

    // 启动 Gin 服务器
    go func() {
        if err := r.Run(":8090"); err != nil {
            fmt.Printf("Failed to start server: %v\n", err)
        }
    }()

    // 每隔一段时间调用一次 SayHello 函数
    ticker := time.NewTicker(2 * time.Second)
    for {
        if sayHello != nil {
            sayHello()
        }
        <-ticker.C
    }
}

运行程序后,先执行

bash 复制代码
go build -buildmode=plugin -o plugin.so plugin.go
curl "http://localhost:8090/update-plugin?name=plugin.so"

输出1111

修改plugin.go代码,再执行

bash 复制代码
go build -buildmode=plugin -o plugin.so plugin2.go
curl "http://localhost:8090/update-plugin?name=plugin2.so"

输出2222

成功了!!

然而,Go 语言的 plugin 包在热更新方面存在诸多限制:

  • 一次性加载plugin.Open 对于同一个插件文件只能加载一次,若要更新插件,就必须更换文件名。
  • 状态丢失:每次加载新的插件都会创建一个新的实例,旧插件的状态无法保留。
  • 功能受限plugin 包主要用于加载外部插件,无法像 Java Instrumentation 那样对已加载的类的方法体进行细粒度的修改。
  • windows平台暂不支持

结论是:

Go的plugin机制在生产环境实现热更新,还有很长一段路要走。目前的功能完全是鸡肋!!

相关推荐
cui_win10 小时前
【基础】Golang语言开发环境搭建(Linux主机)
linux·golang·运维开发
叹一曲当时只道是寻常13 小时前
Softhub软件下载站实战开发(十):实现图片视频上传下载接口
golang·go·音视频
qq_1682789519 小时前
Protobuf在游戏开发中的应用:TypeScript + Golang 实践
服务器·golang·游戏引擎
大模型铲屎官10 天前
【Go语言-Day 7】循环控制全解析:从 for 基础到 for-range 遍历与高级控制
开发语言·人工智能·后端·golang·大模型·go语言·循环控制
mxpan11 天前
深入探究 Go 语言中使用 SQLite 数据库
数据库·golang·sqlite
唯独不开心11 天前
GO 语言学习 之 helloWorld
学习·golang
Go Dgg11 天前
Go 语言的堆糖图片爬虫
开发语言·爬虫·golang
{⌐■_■}11 天前
【编程语言】javascript、java、go对比应用场景
java·javascript·golang
IT艺术家-rookie11 天前
golang--数据类型与存储
开发语言·后端·golang
小诸葛的博客12 天前
go语言实现进度条
开发语言·后端·golang