Hotswap
为 go
语言提供了一套相当完整的代码热更解决方案,热更过程不会中断或阻塞任何执行中的函数,更不会重启服务器。此方案建立在 go
语言的 plugin
机制之上。
- 轻松热更代码
- 完全隔离新老版本
- 通过
Plugin.InvokeFunc()
从宿主调用插件中的函数 - 通过
PluginManager.Vault.Extension
和PluginManager.Vault.DataBag
向宿主暴露插件中的数据和函数 - 借助
live function
、live type
和live data
用最新代码执行异步任务 - 支持静态链接插件,以方便调试
- 通过
Export()
向其它插件暴露函数 - 通过
Import()
声明、建立对其它插件的依赖
go install github.com/sandwich-go/hotswap/cli/hotswap
Usage:
hotswap build [flags] <pluginDir> <outputDir> -- [buildFlags]
Examples:
hotswap build plugin/foo bin
hotswap build -v plugin/foo bin -- -race
hotswap build --staticLinking plugin/foo plugin
Flags:
--debug enable debug mode
--exclude string go-regexp matching files to exclude from included
--goBuild if --goBuild=false, skip the go build procedure (default true)
-h, --help help for build
--include string go-regexp matching files to include in addition to .go files
--leaveTemps do not delete temporary files
--livePrefix string case-insensitive name prefix of live functions and live types (default "live_")
--staticLinking generate code for static linking instead of building a plugin
-v, --verbose enable verbose mode
你可以在 demo
目录下找到这些例子。为了更直观的体验,运行 run.sh
启动服务器,再运行 reload.sh
热更插件。
hello
展示了这套方案的基本用法, 包括怎样组织宿主和插件、怎样编译宿主和插件、怎样在服务器启动时加载插件、怎样使用InvokeEach
、以及怎样热更。extension
是个关于自定义扩展的例子,它可以告诉你PluginManager.Vault.Extension
的用法。小提示:WithExtensionNewer()
。livex
比较复杂. 它展示了live function
,live type
和live data
的用法。slink
展示了静态链接的使用方法。在 MacOS 和 Windows 下,用静态链接才能上调试器(delve)调试。trine
是最后一个例子,它展示了插件的依赖机制。
每个插件都要在其根 package 下定义以下函数:
// OnLoad gets called after all plugins are successfully loaded and before the Vault is initialized.
func OnLoad(data interface{}) error {
return nil
}
// OnInit gets called after the execution of all OnLoad functions. The Vault is ready now.
func OnInit(sharedVault *vault.Vault) error {
return nil
}
// OnFree gets called at some time after a reload.
func OnFree() {
}
// Export returns an object to export to other plugins.
func Export() interface{} {
return nil
}
// Import returns an object indicating the dependencies of the plugin.
func Import() interface{} {
return nil
}
// InvokeFunc invokes the specified function.
func InvokeFunc(name string, params ...interface{}) (interface{}, error) {
return nil, nil
}
// Reloadable indicates whether the plugin is reloadable.
func Reloadable() bool {
return true
}
1. Reloadable
2. Export
3. Import
4. OnLoad
5. Vault Initialization
6. OnInit
- 编译宿主程序时,要加上环境变量
CGO_ENABLED=1
,并指定编译参数-trimpath
。 - 不要在可热更的插件里定义全局变量,除非这些变量从不改变,或(随时)丢弃其值无不良影响。
- 不要在插件里启动长时间运行的 goroutine,否则可能导致部分代码无法热更。
- 小心那些在插件里定义的类型,程序运行时,
go
认为不同插件版本中的同一类型是不同类型,跨版本赋值、拆箱是行不通的。你可以借助live function
,live type
和live data
规避这一陷阱。 - 宿主代码不要 import 任何插件的任何 package;任何插件都不要 import 其它插件的任何 package。
- 热更后,旧版插件会继续留在内存中,永不释放,这是
plugin
的限制导致的。不过你有个清理缓存的机会:OnFree
。 - 必须用
git
和go module
管理代码。 - 强烈建议:用同一个代码仓库管理宿主程序和所有插件。
live function
是以live_
为名字前缀(大小写不敏感)的函数,所有这类函数都会被自动收集起来并存入PluginManager.Vault.LiveFuncs
。例如:
func live_Foo(jobData live.Data) error {
return nil
}
live type
是以live_
为名字前缀(大小写不敏感)的(struct)类型,所有这类 struct 都会被自动收集起来并存入PluginManager.Vault.LiveTypes
。例如:
type Live_Bar struct {
N int
}
live data
是个类型隔离器。你可以在创建异步任务时把任务数据转成live data
对象,再在执行任务时把数据恢复回来。- 例子
livex
包含更多细节。
- 怎样用调试器调试插件?
构建plugin时用静态链接 --staticLink
,会使用模版生成hotswap.staticPlugins.go
。在宿主代码中传入hotswap.WithStaticPlugins
, 会将插件代码编译到宿主程序中运行,调用方式和plugin so模式一致。更多信息请参考演示程序 slink
。
hotswap
能在 Windows 上工作吗?
可以,构建时用静态链接 --staticLink
。不过,在 Windows 上无法热更,因为 go
语言的 plugin
机制不支持 Windows。