Go中模板方法模式通过接口定义窄职责钩子、结构体封装固定流程骨架、函数字段或接口字段注入可变行为实现;必须显式校验钩子非空,禁用具体类型嵌入,入口方法(如Execute)须属结构体而非接口。

Go 里没有继承,但模板方法模式完全能用接口 + 组合稳稳落地。关键不是“怎么模拟 Java”,而是“谁控制流程、谁提供细节”——骨架必须由结构体或函数掌控执行顺序,可变部分必须通过接口或函数参数注入,且调用点必须显式、不可被嵌入自动覆盖。
为什么不能在结构体里直接嵌入具体类型
常见错误是写 type TemplateRunner struct { FileProcessor },以为这样就能复用 FileProcessor 的 Process() 方法。结果发现:TemplateRunner.Process() 要么编译报错(ambiguous selector),要么静默走基类实现,根本调不到你想要的定制逻辑。
- 嵌入具体类型 → 方法集被静态绑定,无法运行时替换
-
TemplateRunner一旦嵌入FileProcessor,就和它强耦合,没法再换DBProcessor或HTTPProcessor - 单元测试时无法注入 mock,所有测试都得跑真实 I/O
- 真正该嵌入的是接口字段,比如
Processor Processor,而不是具体实现
接口定义必须窄且职责明确
别定义一个大而全的 Handler 接口,里面塞十几个方法。模板方法只关心它要调用的那几个钩子,多一个都算污染。
- 接口名建议带后缀,如
DataProcessor、Workflow、Exporter,一眼看出用途 - 每个方法名要体现意图:
Validate()、Transform()、Persist()比Step1()、Do()更可靠 - 可选步骤不要靠 panic 或空实现跳过,统一返回
error或加ShouldXxx() bool方法 - 避免在接口里暴露模板方法不需要的方法(比如
Log()、Retry()),否则违反里氏替换
Run() 方法里怎么安全调用钩子
Run() 是模板方法的核心,但它不能假设所有钩子都非 nil。Go 接口零值就是 nil,直接调用会 panic。
立即学习“go语言免费学习笔记(深入)”;
- 别写
if p.Cleanup != nil—— 接口变量本身不能和nil比较,这是常见误判 - 正确做法:在骨架方法内做空判断,例如
if p.Cleanup != nil { p.Cleanup() }是错的;应改为if cleanup := p.Cleanup; cleanup != nil { cleanup() } - 更推荐让钩子方法默认返回
nilerror,由具体实现决定是否跳过,骨架不干预语义 - 错误处理要透传:某步
Validate()失败,后续步骤就不该执行,Run()应立刻返回
函数参数 vs 接口字段:怎么选
轻量场景优先用函数参数,重逻辑才上接口。这不是风格问题,是维护成本问题。
- 单次定制、无状态、差异小 → 用
func(*Order) error这类函数类型,比如校验规则切换 - 需要复用内部状态(如 HTTP client、cache map)、多次调用、跨包复用 → 定义窄接口,如
OrderProcessor - 别为了“看起来像模板方法”硬造接口,如果只有两个实现且不会扩展,函数参数更直白
- 接口字段必须导出(首字母大写),否则外部无法赋值:
Processor Processor✅,processor Processor❌
最容易被忽略的一点:模板方法的入口(比如 Execute() 或 Run())永远不该出现在接口里。它必须属于骨架结构体或独立函数——否则你就把流程控制权交出去了,那就不叫模板方法,叫普通回调。


















