Go中可用嵌入接口+显式委托模拟模板方法:定义Processor接口含Validate/Process/Cleanup,TemplateRunner持该接口字段,Run()按序调用并空判可选钩子,避免嵌入具体类型以保多态与可测性。

Go 里没有继承,怎么模拟模板方法?
Go 不支持类继承,所以不能像 Java 那样靠抽象基类 + abstract 方法定义模板流程。但你可以用「嵌入接口 + 嵌入结构体 + 显式委托」组合出等效行为:把算法骨架放在一个结构体里,把可变步骤抽成接口方法,再让具体类型实现那些接口并嵌入骨架。
关键不是“复用结构体字段”,而是“复用控制流逻辑”——比如 Run() 先校验、再处理、最后清理,这三步顺序固定,只有中间的 Process() 由子类型决定。
- 必须定义一个接口(如
Processor),包含所有钩子方法(Validate()、Process()、Cleanup()) - 定义一个“模板结构体”(如
TemplateRunner),字段是该接口类型,不嵌入具体实现 - 具体类型(如
FileProcessor)实现接口,并在调用处传给自己:tr := TemplateRunner{p: &FileProcessor{...}}
为什么不能直接嵌入具体结构体?
如果在 TemplateRunner 里直接嵌入 FileProcessor,就锁死了实现,丧失多态性——后续没法换 DBProcessor 或 HTTPProcessor。更糟的是,一旦嵌入,Go 会自动提升其方法到外层,导致 TemplateRunner.Process() 变成具体实现,无法被替换。
常见错误现象:TemplateRunner 嵌入了 FileProcessor,结果所有子类型都共享同一份 Process(),根本没法定制。
立即学习“go语言免费学习笔记(深入)”;
- 嵌入具体类型 → 失去运行时多态,违反模板方法本意
- 嵌入接口字段 → 保持延迟绑定,
tr.Run()内部调用tr.p.Process()才真正分发 - 接口字段需导出(首字母大写),否则外部无法赋值
Run() 方法里怎么安全调用钩子?
模板方法的核心是 Run() 控制执行顺序,但它必须容忍某些钩子为空(比如不是所有子类型都需要 Cleanup())。别硬写 if p.Cleanup != nil —— Go 接口零值本身就是 nil,直接调用会 panic。
正确做法:在模板结构体方法里做空判断,或要求子类型提供默认空实现(推荐前者,更轻量)。
func (tr *TemplateRunner) Run() error {
if err := tr.p.Validate(); err != nil {
return err
}
if err := tr.p.Process(); err != nil {
return err
}
// Cleanup 是可选的
if tr.p.Cleanup != nil {
tr.p.Cleanup()
}
return nil
}
- 接口方法字段(如
Cleanup func())可以为nil,调用前必须显式判空 - 若钩子是带参数/返回值的函数类型(如
func(string) error),也一样要判空,否则 panic - 不要依赖 defer 在
Run()里注册钩子——defer 绑定的是当前栈帧的值,无法反映后续替换
嵌入结构体字段 vs 接口字段:性能和可测试性差异
用接口字段(p Processor)比嵌入结构体(FileProcessor)多一次间接调用,但现代 Go 编译器常能内联简单接口方法,实际开销极小。真正的优势在可测试性:你可以传入 mock 实现,验证 Run() 是否按序调用了 Validate→Process→Cleanup。
- 接口字段:支持单元测试打桩、支持运行时切换策略、符合依赖倒置
- 嵌入结构体:编译期绑定、无法替换行为、测试时只能测具体类型,模板逻辑被掩盖
- 如果真有性能敏感场景(如每秒百万次调用),且钩子逻辑极简,可考虑泛型约束替代接口,但会牺牲灵活性
context.Context 参数,也会导致接口不匹配——Go 的接口实现是严格签名匹配的,没隐式转换。










