Day 05 / 共 20 天 · 第 1 周 入门与生命周期
SetCtx 与函数式选项
第 1 周收官。今天揭开 SetCtx("name", 选项...) 的实现——那些 ParseConfig(...)、ProcessRequestHeaders(...) 到底是什么。答案是 Go 里优雅的"函数式选项模式",学会它你自己也能写出这种好用的 API。
📍 你在 wasm-go 前 10 天的位置(W1 入门与生命周期 · W2 配置与匹配;W3-4 见 D11-20)
D01 全景·
D02 request-block·
D03 ABI·
D04 三级Context·
D05 SetCtx/选项→
D06 配置解析·
D07 RuleMatcher·
D08 运行时匹配·
D09 全局vs规则·
D10 生命周期→
W3-4 请求处理/高级
🤔 痛点:一个"什么都能配"的构造函数会变成什么样?
插件要能配"怎么解析配置""请求头怎么处理""要不要安全日志""多少请求后重建"……如果全塞进一个构造函数,就成了
New(name, parseFn, reqHeaderFn, reqBodyFn, respFn, logger, safeLog, rebuildN, maxMem, ...)——二十个参数、顺序记不住、大部分还想用默认值只能挨个填 nil。太难用了。💡 本质:函数式选项 = 给芯片"按需插功能卡"
延续机场类比:
SetCtx 是给这台安检芯片装配功能。每个 Option(ParseConfig(...)、ProcessRequestHeaders(...)…)是一张功能卡,Apply 就是"把卡插进对应插槽"。你要哪个功能就插哪张卡、顺序随意、不插就用默认——这正是 gRPC 等成熟库都在用的写法。今天是第 1 周收官:把"怎么装配一台插件"彻底看透。L01
SetCtx 做什么
plugin_wrapper.go:120-126:
func SetCtx[PluginConfig any](pluginName string, options ...CtxOption[PluginConfig]) {
proxywasm.SetVMContext(NewCommonVmCtx(pluginName, options...))
}
读法:
options ...CtxOption 是"可变参数"——你能传 0 个或任意多个选项。NewCommonVmCtx 用这些选项配好一个 CommonVmCtx,再 proxywasm.SetVMContext 注册给底层。整个插件的行为,就由你传的这组选项决定。L02
CtxOption 接口
plugin_wrapper.go:128-130:
type CtxOption[PluginConfig any] interface {
Apply(*CommonVmCtx[PluginConfig])
}
什么是"函数式选项模式"?
每个选项(
ParseConfig(f)、ProcessRequestHeaders(f)…)都返回一个实现了 Apply 方法的对象。构造时挨个调 opt.Apply(ctx),让每个选项把自己"装配"到 ctx 上。好处:加新选项不用改 SetCtx 的签名,参数顺序随意、可选可省。这是 Go 里配置复杂对象的最佳实践(gRPC、很多库都这么做)。对比"一个有 20 个参数的构造函数"——那种没法记、没法扩展。L03
Apply 模式(以 ParseConfig 为例)
plugin_wrapper.go:132-166:ParseConfig(f) 返回一个 parseConfigOption,它的 Apply 把回调装进 ctx:
func ParseConfig[PluginConfig any](f ParseConfigFunc[PluginConfig]) CtxOption[PluginConfig] {
return &parseConfigOption[PluginConfig]{f: f} // 返回选项对象
}
func (o parseConfigOption[PluginConfig]) Apply(ctx *CommonVmCtx[PluginConfig]) {
// ... 把你的 f 包装后赋给 ctx.parseConfig ...
ctx.parseConfig = func(context PluginContext, configBytes []byte, config *PluginConfig) error {
return o.f(gjson.ParseBytes(configBytes), config) // 帮你把 bytes 转成 gjson
}
}
读法:看这个包装——你写的
parseConfig(json, config) 只关心 gjson,但底层 SDK 收到的是 []byte。Apply 帮你补上 gjson.ParseBytes 这步转换。选项模式让 SDK 能在"你的简单函数"外面套一层"适配底层"的壳,你无感。图注:
NewCommonVmCtx 挨个调 opt.Apply(ctx),每张功能卡把自己写进 ctx 的对应字段。加新卡不用改 SetCtx 签名。📝 简化版 → 真实版:如果让你自己实现选项模式
你可能会写:
真实 wasm-go 只多做两件事:① 把
type Option func(*Ctx),func WithParse(f) Option { return func(c){ c.parse=f } },构造时 for _,o:=range opts { o(c) }。真实 wasm-go 只多做两件事:① 把
func(*Ctx) 换成 interface{ Apply(*Ctx) }(能带字段、便于区分新旧回调);② Apply 里多包一层适配(如补 gjson.ParseBytes)。骨架完全一样——你已经懂了它的核心。L04
8 个 Process 钩子
请求处理相关的选项一共 8 个(plugin_wrapper.go:232-397),覆盖请求/响应 × 头/体/流式/结束:
| 选项 | 触发时机 |
|---|---|
| ProcessRequestHeaders | 请求头到达 |
| ProcessRequestBody | 请求体到达(整块) |
| ProcessStreamingRequestBody | 请求体到达(流式分块) |
| ProcessResponseHeaders | 响应头到达 |
| ProcessResponseBody | 响应体到达(整块) |
| ProcessStreamingResponseBody | 响应体到达(流式分块) |
| ProcessStreamDone | 流结束 |
| ParseConfig / ParseOverrideConfig | 配置加载 |
读法:你只注册需要的钩子——只关心请求头就只注册 ProcessRequestHeaders。没注册的钩子对应的 body 网关不会读(省内存,呼应 Day 04)。Day 11 会详讲这些阶段的顺序和 Action 语义。
L05
新旧两套 API
你会注意到很多选项有两个版本,比如 ProcessRequestHeaders 和 ProcessRequestHeadersBy(标了 // Deprecated,:247-253):
// 旧(Deprecated):回调多一个 log 参数
type oldOnHttpHeadersFunc[PluginConfig any] func(context HttpContext, config PluginConfig, log log.Log) types.Action
// 新:回调不带 log(用全局 log 包函数即可)
type onHttpHeadersFunc[PluginConfig any] func(context HttpContext, config PluginConfig) types.Action
为什么演进出新 API?
旧版每个回调都要手动接收并传递
log 参数,啰嗦。新版把日志改成全局的 log.Infof(...)(Day 16 讲),回调签名更干净。SDK 为了不破坏老插件,用 Apply 里的 oldF/f 分支同时支持两套——老代码不用改,新代码更简洁。这是成熟 SDK 的向后兼容做法。你写新插件用不带 By 后缀的新版即可。👶 小白:我读别人的老插件,回调里带着 log log.Log 参数,我照抄会不会报错?
👨🏫 老师:不会报错——带 log 参数的是旧版(...By 系列),SDK 仍支持。但你写新插件时,直接用不带 By 的版本、回调里少一个 log 参数,需要打日志时用全局 log.Infof(...)(Day 16)。新老两套并存,看到别人带 log 别慌,那只是老写法。
⚠️ 常见误解:看到
Deprecated 以为"这函数马上要被删、不能用"。其实在 SDK 里 Deprecated 通常只是"有更好的新版了,新代码别再用",老函数为了兼容会长期保留,跑起来完全正常。L06
泛型贯穿
注意到处都是 [PluginConfig any]——这是 Go 泛型。SetCtx、CtxOption、各回调类型都带这个类型参数。
泛型在这里解决什么?
你的配置类型(比如
RequestBlockConfig)是自定义的。泛型让 SDK 不写死配置类型,而是"你传什么配置类型,parseConfig 和各钩子就收到什么类型"——类型安全、无需强转。SetCtx 的类型参数由你传的回调函数自动推断(Go 编译器帮你填)。所以 request-block 里 wrapper.ParseConfig(parseConfig) 就自动确定了 PluginConfig = RequestBlockConfig。这是 Go 1.18+ 泛型的漂亮应用。L07
其他选项
除了配置和请求钩子,还有一批"能力开关"选项(后面几周会用到):
WithLogger(:409):自定义日志实现。EnableSafeLog(:431):开启安全日志,屏蔽敏感信息(Day 16)。WithRebuildAfterRequests(:444)/WithRebuildMaxMemBytes(:459):性能重建(Day 19)。WithMaxRequestsPerIoCycle(:490):IO 并发限制(Day 19)。PrePluginStartOrReload(:515):插件启动前的钩子。
读法:全都是同一个选项模式——返回一个带
Apply 的对象。SDK 的所有可配置行为都统一走这套机制,一致、可扩展。你要给插件加个能力开关,照葫芦画瓢加个 Option 即可。L08
今日小结 + 动手(第 1 周收官)
🧠 第 1 周你应该能回答
- 函数式选项模式怎么工作?比多参数构造函数好在哪?
Apply里为什么要包一层(如补 gjson.ParseBytes)?- 8 个 Process 钩子覆盖哪些阶段?
- 为什么有新旧两套 API?泛型
PluginConfig解决什么?
✋ 动手
cd /Users/bitmart/work/codes/github/higress-group/wasm-go
sed -n '120,170p' pkg/wrapper/plugin_wrapper.go
grep -n "func Process\|func Parse\|func With\|func Enable" pkg/wrapper/plugin_wrapper.go
下周预告 · 第 2 周:配置与匹配——ParseConfig 家族的各种变体、
RuleMatcher 怎么按路由/域名/服务匹配不同配置、运行时 GetMatchConfig 怎么选、以及 OnPluginStart 生命周期全貌。