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 是给这台安检芯片装配功能。每个 OptionParseConfig(...)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-166ParseConfig(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 收到的是 []byteApply 帮你补上 gjson.ParseBytes 这步转换。选项模式让 SDK 能在"你的简单函数"外面套一层"适配底层"的壳,你无感。
装配流程:一张张功能卡被 Apply 插进空白芯片 ParseConfig(f) ProcessRequestHeaders(g) EnableSafeLog() 你传的功能卡(顺序随意) CommonVmCtx .parseConfig ← f .onHttpRequestHeaders ← g .needSafeLog ← true (装配好的芯片) for opt := range options { opt.Apply(ctx) }
图注:NewCommonVmCtx 挨个调 opt.Apply(ctx),每张功能卡把自己写进 ctx 的对应字段。加新卡不用改 SetCtx 签名。
📝 简化版 → 真实版:如果让你自己实现选项模式 你可能会写: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

你会注意到很多选项有两个版本,比如 ProcessRequestHeadersProcessRequestHeadersBy(标了 // 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 泛型。SetCtxCtxOption、各回调类型都带这个类型参数。

泛型在这里解决什么? 你的配置类型(比如 RequestBlockConfig)是自定义的。泛型让 SDK 不写死配置类型,而是"你传什么配置类型,parseConfig 和各钩子就收到什么类型"——类型安全、无需强转。SetCtx 的类型参数由你传的回调函数自动推断(Go 编译器帮你填)。所以 request-blockwrapper.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 生命周期全貌。
← Day 04 Day 06 · 配置解析 →