前言
由于 Golang 是静态编译,所以 plugin 机制一直是一个难解的问题,官方提供的 plugin 机制又特别难用,但插件无疑是扩展原始功能的一种最方便的途径。于是乎,各路软件自家都有各种插件机制。caddy 使用 xcaddy 来实现插件机制,我们来看看它是如何做的。
结论
首先上来先给结论,它必须重新编译,没办法,这也是一个必然选择。很多人听到这,只能叹气一下。
使用方式
使用命令安装 xcaddy
go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest
然后使用命令选择需要的插件进行重新编译:
代码语言:javascript复制xcaddy build master
--with github.com/caddyserver/ntlm-transport
其中 master 是指定 caddy 的版本
原理
其实原理非常简单,看源码一下就明白了
1. 生成 main.go
在 xcaddy
运行 build 命令的时候首先会创建一个临时文件夹,然后按照模板写入一个 main.go 文件:
const mainModuleTemplate = `package main
import (
caddycmd "{{.CaddyModule}}/cmd"
// plug in Caddy modules here
_ "{{.CaddyModule}}/modules/standard"
{{- range .Plugins}}
_ "{{.}}"
{{- end}}
)
func main() {
caddycmd.Main()
}
`
其中包含你需要打包进来的 moudles 放在 import 中,以匿名的方式引入。
并且在 main 函数中运行 caddycmd.Main()
也就是 caddy 主项目的主函数。
2. 运行 go mod tidy 和 go build
然后 xcaddy 就直接运行 go mod tidy 进行 go 相关依赖下载,然后使用 go build 根据相关参数进行构建打包。
3. 插件的注册原理
那为什么只需要引入包,即可完成插件的注册呢?它里面的注册原理其实也很简单:
代码语言:javascript复制package mymodule
import "github.com/caddyserver/caddy/v2"
func init() {
caddy.RegisterModule(Gizmo{})
}
// Gizmo is an example; put your own type here.
type Gizmo struct {
}
// CaddyModule returns the Caddy module information.
func (Gizmo) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
ID: "foo.gizmo",
New: func() caddy.Module { return new(Gizmo) },
}
}
插件注册是通过 caddy.RegisterModule()
来实现的,只需要将这个方法放在 init()
函数中,由于在第二步 import 语句中引入了插件,则运行时就会有依赖并执行 init 函数,从而实现注册。
4. 插件的使用原理
既然插件已经通过 RegisterModule
方法注册上了,那么如何使用对应的插件呢?
首先 caddy 定义了形如下面类似的接口:
type Unmarshaler interface {
UnmarshalCaddyfile(d *Dispenser) error
}
然后插件注册的方法 RegisterModule 中会将插件保存到 modules 这个 map 中
代码语言:javascript复制func RegisterModule(instance Module) {
mod := instance.CaddyModule()
if mod.ID == "" {
panic("module ID missing")
}
if mod.ID == "caddy" || mod.ID == "admin" {
panic(fmt.Sprintf("module ID '%s' is reserved", mod.ID))
}
if mod.New == nil {
panic("missing ModuleInfo.New")
}
if val := mod.New(); val == nil {
panic("ModuleInfo.New must return a non-nil module instance")
}
modulesMu.Lock()
defer modulesMu.Unlock()
if _, ok := modules[string(mod.ID)]; ok {
panic(fmt.Sprintf("module already registered: %s", mod.ID))
}
modules[string(mod.ID)] = mod
}
只要插件实现了对应的接口,并注册上去,caddy 会通过下面的方法获取:
代码语言:javascript复制// GetModule returns module information from its ID (full name).
func GetModule(name string) (ModuleInfo, error) {
modulesMu.RLock()
defer modulesMu.RUnlock()
m, ok := modules[name]
if !ok {
return ModuleInfo{}, fmt.Errorf("module not registered: %s", name)
}
return m, nil
}
然后通过反射进行类型推导,转换为对应的接口类型,从而进行使用:
代码语言:javascript复制// 获取模块
mod, err := caddy.GetModule("http.matchers." matcherName)
if err != nil {
return fmt.Errorf("getting matcher module '%s': %v", matcherName, err)
}
// 转换为对应接口类型
unm, ok := mod.New().(caddyfile.Unmarshaler)
if !ok {
return fmt.Errorf("matcher module '%s' is not a Caddyfile unmarshaler", matcherName)
}
// 调用接口对应的实现方法
err = unm.UnmarshalCaddyfile(caddyfile.NewDispenser(tokens))
if err != nil {
return err
}
设计思路
如果让我以这样的思路去设计插件,我一定会说,先把源码下载下来,然后再 main 函数中添加对应的模块依赖,然后重新编译。这样的问题是,你需要手动下载并编辑代码,不太友好和方便,而 caddy 提供了另一种解决思路,就是将依赖倒置给 xcaddy。由 xcaddy 生成一个新的项目,而新项目中依赖了 caddy 原始项目和 需要的插件。这样的好处是,用户不用关心源码,原项目不需要修改,只需关心插件即可。
总结
虽然 golang 中的插件思路往往都有些看着难办,但这也不失为一种解决方式。