在Go中实现插件热重载可以通过plugin包结合文件系统监控来实现。以下是一个完整的示例:
package main
import (
"fmt"
"log"
"plugin"
"sync"
"time"
"github.com/fsnotify/fsnotify"
)
type PluginManager struct {
mu sync.RWMutex
plugins map[string]*plugin.Plugin
symbols map[string]map[string]interface{}
filePath string
}
func NewPluginManager() *PluginManager {
return &PluginManager{
plugins: make(map[string]*plugin.Plugin),
symbols: make(map[string]map[string]interface{}),
}
}
func (pm *PluginManager) LoadPlugin(path string) error {
pm.mu.Lock()
defer pm.mu.Unlock()
p, err := plugin.Open(path)
if err != nil {
return fmt.Errorf("加载插件失败: %v", err)
}
// 获取插件符号
syms := make(map[string]interface{})
sym, err := p.Lookup("Version")
if err == nil {
syms["Version"] = sym
}
sym, err = p.Lookup("Execute")
if err == nil {
syms["Execute"] = sym
}
pm.plugins[path] = p
pm.symbols[path] = syms
log.Printf("插件已加载: %s", path)
return nil
}
func (pm *PluginManager) ReloadPlugin(path string) error {
pm.mu.Lock()
defer pm.mu.Unlock()
// 先关闭旧插件(Go插件无法显式关闭,但可以替换引用)
delete(pm.plugins, path)
delete(pm.symbols, path)
// 加载新版本
p, err := plugin.Open(path)
if err != nil {
return fmt.Errorf("重新加载插件失败: %v", err)
}
syms := make(map[string]interface{})
sym, err := p.Lookup("Version")
if err == nil {
syms["Version"] = sym
}
sym, err = p.Lookup("Execute")
if err == nil {
syms["Execute"] = sym
}
pm.plugins[path] = p
pm.symbols[path] = syms
log.Printf("插件已重新加载: %s", path)
return nil
}
func (pm *PluginManager) GetSymbol(path, name string) (interface{}, error) {
pm.mu.RLock()
defer pm.mu.RUnlock()
syms, ok := pm.symbols[path]
if !ok {
return nil, fmt.Errorf("插件未找到: %s", path)
}
sym, ok := syms[name]
if !ok {
return nil, fmt.Errorf("符号未找到: %s", name)
}
return sym, nil
}
func (pm *PluginManager) WatchPlugin(path string) {
watcher, err := fsnotify.NewWatcher()
if err != nil {
log.Fatal(err)
}
defer watcher.Close()
err = watcher.Add(path)
if err != nil {
log.Fatal(err)
}
for {
select {
case event, ok := <-watcher.Events:
if !ok {
return
}
if event.Op&fsnotify.Write == fsnotify.Write {
log.Printf("检测到插件文件变更: %s", event.Name)
time.Sleep(100 * time.Millisecond) // 等待文件写入完成
if err := pm.ReloadPlugin(event.Name); err != nil {
log.Printf("重新加载失败: %v", err)
}
}
case err, ok := <-watcher.Errors:
if !ok {
return
}
log.Printf("监控错误: %v", err)
}
}
}
// 插件示例代码 (plugin/example.go)
// 编译命令: go build -buildmode=plugin -o example.so plugin/example.go
/*
package main
var Version = "1.0.0"
func Execute() string {
return "Hello from plugin v" + Version
}
*/
func main() {
pm := NewPluginManager()
// 初始加载插件
pluginPath := "./example.so"
if err := pm.LoadPlugin(pluginPath); err != nil {
log.Fatal(err)
}
// 启动文件监控
go pm.WatchPlugin(pluginPath)
// 定期执行插件功能
ticker := time.NewTicker(2 * time.Second)
defer ticker.Stop()
for range ticker.C {
if sym, err := pm.GetSymbol(pluginPath, "Execute"); err == nil {
if executeFunc, ok := sym.(func() string); ok {
result := executeFunc()
fmt.Printf("插件执行结果: %s\n", result)
}
}
if sym, err := pm.GetSymbol(pluginPath, "Version"); err == nil {
if version, ok := sym.(*string); ok {
fmt.Printf("当前插件版本: %s\n", *version)
}
}
}
}
这个实现包含以下关键部分:
- PluginManager结构体:管理插件的加载和重载,使用读写锁保证并发安全
- LoadPlugin方法:初始加载插件并缓存符号
- ReloadPlugin方法:重新加载插件的新版本
- WatchPlugin方法:使用fsnotify监控插件文件变化
- 主程序:定期调用插件函数并显示版本信息
需要安装依赖:
go get github.com/fsnotify/fsnotify
编译插件:
go build -buildmode=plugin -o example.so plugin/example.go
注意事项:
- Go插件系统目前只支持Linux、macOS和FreeBSD
- 插件必须使用相同的Go版本和依赖编译
- 旧插件无法显式卸载,但会被垃圾回收器回收
- 文件监控需要适当的延迟确保文件写入完成