go-plugin 插件系统详解

go-plugin 插件系统详解

基于 HashiCorp go-plugin 库的插件系统,通过 RPC 实现主程序(Host)与独立进程(Plugin)之间的通信。

一、架构概览

1
2
3
4
5
6
┌─────────────────┐         RPC          ┌─────────────────┐
│ Host (main) │ ◄─────────────────► │ Plugin (子进程) │
│ │ │ │
│ - 加载插件进程 │ │ - 实现接口 │
│ - 调用 RPC │ │ - 提供服务 │
└─────────────────┘ └─────────────────┘

核心特点:

  • 进程隔离:Plugin 崩溃不影响 Host
  • 语言无关:Plugin 可以用任何语言实现(只要支持 RPC)
  • 动态加载:运行时加载/替换插件

二、核心组件

1. 定义接口 (shared/xxx_interface.go)

1
2
3
type Provider interface {
Provide() string
}

该接口是 Plugin 端和 Host 端都需实现的。

2. 四个关键结构体的协作

在示例中有4个结构体,一同协作实现了插件系统最核心的工作流;默认情况下, Host 作为 RPC 客户端,Plugin 作为 RPC 服务端提供服务。

结构体 位置 角色
JsonProvider Plugin 端 真实业务实现
ProvideRPCServer Plugin 端 RPC 服务端,接收请求
ProvideClientRPC Host 端 RPC 客户端代理,转发调用
ProvidePlugin 胶水层 连接两端的桥梁

协作流程图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
┌─────────────────────────── HOST 进程 ───────────────────────────┐
│ │
│ ProvideClientRPC │
│ ┌─────────────────────────────────┐ │
│ │ 实现 Provider 接口 │ │
│ │ Provide() 方法 │ │
│ │ 内部持有 rpc.Client │ ──┐ │
│ └─────────────────────────────────┘ │ │
│ ▼ │
│ ┌──────────────────┐ │
│ │ 网络 RPC 调用 │ │
│ └──────────────────┘ │
└────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────── PLUGIN 进程 ───────────────────────┐
│ │
│ ┌──────────────────┐ │
│ │ 网络 RPC 服务 │ │
│ └──────────────────┘ │
│ │ │
│ ProvideRPCServer ▼ │
│ ┌─────────────────────────────────┐ │ │
│ │ 实现 net/rpc.Server 接口 │ │ │
│ │ Provide() 方法 │◄──┘ │
│ │ 内部持有 JsonProvider │ │
│ └─────────────────────────────────┘ │
│ │ │
│ ▼ │
│ JsonProvider (真实业务实现) │
│ ┌─────────────────────────────────┐ │
│ │ 实现 Provider 接口 │ │
│ │ Provide() 返回实际结果 │ │
│ └─────────────────────────────────┘ │
└────────────────────────────────────────────────────────────────┘

ProvidePlugin 是"胶水":
• Server() 方法 → 返回 ProvideRPCServer
• Client() 方法 → 返回 ProvideClientRPC

调用流程:

  1. Host 调用 ProvideClientRPC.Provide()
  2. ProvideClientRPC 通过 rpc.Client 发送网络请求
  3. Plugin 端的 ProvideRPCServer.Provide() 接收请求
  4. ProvideRPCServer 调用 JsonProvider.Provide() 获取真实结果
  5. 结果沿原路返回给 Host

3. Plugin 实现 (shared/xxx_interface.go)

1
2
3
4
5
6
7
8
9
10
11
type ProvidePlugin struct {
Impl Provider
}

func (p *ProvidePlugin) Server(broker *plugin.MuxBroker) (interface{}, error) {
return &ProvideRPCServer{Impl: p.Impl}, nil
}

func (p *ProvidePlugin) Client(broker *plugin.MuxBroker, client *rpc.Client) (interface{}, error) {
return &ProvideClientRPC{client: client}, nil
}

4. Host 端 (main.go)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 启动插件进程
client := plugin.NewClient(&plugin.ClientConfig{
HandshakeConfig: handshakeConfig,
Plugins: pluginMap,
Cmd: exec.Command("./plugin/json"),
Logger: logger,
})
defer client.Kill()

// 获取 RPC 客户端
rpcClient, err := client.Client()

// 提取插件实例
raw, err := rpcClient.Dispense("provider")
greeter := raw.(shared.Provider)

// 调用插件
greeter.Provide()

5. Plugin 端 (plugin/main.go)

1
2
3
4
5
6
7
8
9
10
11
12
func main() {
jsonA := &JsonProvider{logger: logger}

pluginMap := map[string]plugin.Plugin{
"provider": &shared.ProvidePlugin{Impl: jsonA},
}

plugin.Serve(&plugin.ServeConfig{
HandshakeConfig: handshakeConfig,
Plugins: pluginMap,
})
}

三、Dispense 的工作原理

rpcClient.Dispense("provider") 中的 “provider” 是 key:

位置 代码 说明
Host 端 rpcClient.Dispense("provider") 请求 key 为 “provider” 的插件
Plugin 端 pluginMap = {"provider": &ProvidePlugin{...}} 用 “provider” 注册

流程:

  1. Host 调用 Dispense("provider")
  2. go-plugin 框架向 Plugin 发送请求,携带 key
  3. Plugin 查找 pluginMap[key],调用 Server() 返回 RPCServer
  4. Host 端调用 Client() 获取代理对象
  5. 代理对象类型断言为接口使用

四、Handshake 机制

HandshakeConfig 结构

1
2
3
4
5
var handshakeConfig = plugin.HandshakeConfig{
ProtocolVersion: 1, // 协议版本
MagicCookieKey: "BASIC_PLUGIN", // Cookie 键
MagicCookieValue: "hello", // Cookie 值
}

三个属性的作用

属性 作用
ProtocolVersion 校验 Host 和 Plugin 的协议版本是否一致,不一致则拒绝连接
MagicCookieKey 用于验证的 key(相当于校验字段名)
MagicCookieValue 用于验证的 value(相当于校验密码)

工作流程

1
2
3
4
5
6
7
8
9
10
Host 启动 Plugin 进程
↓
发送 HandshakeConfig 信息(包含 version、cookie)
↓
Plugin 验证:
- version 是否匹配?
- cookie key/value 是否正确?
↓
验证失败 → 显示友好错误信息 → 进程退出
验证成功 → 建立 RPC 连接

验证失败的场景

  1. 版本不匹配 - Host 用 v1,Plugin 用 v2
  2. 误执行插件目录 - 用户直接运行 ./plugin/ 而不是可执行文件
  3. 错误的插件 - 加载了不相关的可执行文件

示例错误信息

1
2
3
4
5
6
错误的 Plugin:
This plugin is not a HashiCorp go-plugin plugin.
It appears to be a directory or other invalid plugin.

版本不匹配:
Incompatible api version. Plugin: 2, Host: 1

总结

  • 不是安全机制 - Cookie 是明文传输,可被伪造
  • 是防错机制 - 防止用户因为操作错误导致困惑
  • 建议保持一致 - Host 和 Plugin 两端的 HandshakeConfig 应完全相同

五、双向通信

方向 是否支持 说明
Host → Plugin ✅ 默认 Plugin 提供服务
Plugin → Host ✅ 可选 需用 MuxBroker 建立回调通道

通过 MuxBroker 可以让 Plugin 主动回调 Host。

六、性能开销

调用方式 延迟
直接函数调用 ~1-10 ns
go-plugin RPC ~50-500 μs
本地网络 RPC ~100μs - 1ms

开销来源:

  • 序列化(gob 编码)
  • 进程间通信(socket)
  • 上下文切换
  • 反射开销

适用场景:调用频率不高的、需要进程隔离的场景。

七、”Plugin.Provide” 中的 “Plugin” 是什么?

在 provider_interface.go:19 中:

1
err := p.client.Call("Plugin.Provide", new(interface{}), &resp)

"Plugin.Provide" 是 net/rpc 的调用格式:"类型名.方法名"。

那为什么是 “Plugin.Provide” ?

“Plugin” 是 go-plugin 框架硬编码的服务名。客户端调用 "Plugin.Provide" 正是因为服务端用
server.RegisterName("Plugin", impl) 注册了服务。

源码位置

在 go-plugin 库的 rpc_server.go:195 行:

1
2
3
4
5
6
7
8
9
10
11
12
go func () {
...
serve(conn, "Plugin", impl) // 硬编码 "Plugin"
}()

func serve(conn io.ReadWriteCloser, name string, v interface{}) {
server := rpc.NewServer()
if err := server.RegisterName(name, v); err != nil {
...
}
server.ServeConn(conn)
}

go-plugin 插件系统详解
https://sunshinechen.cn/2026/03/11/go-plugin, Go实现的插件库/
作者
jianhao
发布于
2026年3月11日
许可协议