go中可用接口+组合实现模板方法:定义processor接口声明validate/transform/persist等可变步骤,baseprocessor嵌入processor字段并在execute中编排调用顺序,用户实现接口并注入,确保扩展点清晰、可测试且解耦。

Go 里没有继承,怎么写模板方法?
Go 不支持类继承,所以不能像 Java 那样靠抽象基类 + final 方法强制子类实现钩子。但模板方法的本质是「定义算法骨架,把可变步骤延迟到具体类型实现」——这完全可以用接口 + 嵌入 + 组合做到,而且更清晰。
关键不是“模拟继承”,而是让调用方只依赖一个稳定接口(比如 Processor),而具体逻辑由嵌入的字段或组合对象提供。
- 别试图用匿名字段“覆盖”父方法;Go 的方法调用是静态绑定的,嵌入只是语法糖,不是运行时多态
- 真正起作用的是:主结构体持有一个接口字段(如
stepRunner),在Execute()中按顺序调用它定义的方法 - 所有可变步骤必须声明在接口里,且命名体现职责(如
Validate()、Transform()、Persist()),避免泛泛叫DoStep1()
用嵌入+接口实现可扩展骨架
典型做法是定义一个「骨架结构体」,嵌入一个接口类型的字段,并在它的公开方法中编排流程。这个接口就是留给用户实现的扩展点。
例如:
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
type Processor interface {
Validate() error
Transform() error
Persist() error
}
type BaseProcessor struct {
Runner Processor // ← 关键:运行时注入具体实现
}
func (b *BaseProcessor) Execute() error {
if err := b.Runner.Validate(); err != nil {
return err
}
if err := b.Runner.Transform(); err != nil {
return err
}
return b.Runner.Persist()
}
-
BaseProcessor不实现任何业务逻辑,只负责流程控制和错误传递 - 用户只需实现
Processor接口,然后传给BaseProcessor{Runner: &MyCustomProc{}} - 如果某步可选,接口方法应返回
error或明确布尔值(如ShouldSkipPersist() bool),不要靠 panic 或空实现隐式跳过
为什么不用 embed struct 而用 interface 字段?
有人会想直接嵌入一个具体结构体(如 embed MyStepImpl),但这会导致耦合和测试困难:骨架和实现绑死,无法替换、mock 或复用不同组合。
- 嵌入具体类型 → 编译期强依赖,单元测试只能跑真实逻辑
- 用接口字段 → 可注入 mock、stub、装饰器(如加日志、重试),也方便做 A/B 测试不同策略
- 注意:接口方法签名要稳定。如果后续加新钩子(如
BeforePersist()),旧实现会编译失败——这是好事,说明扩展点变更需要显式适配
常见坑:方法调用顺序错乱 or 空指针 panic
最常出问题的是初始化遗漏或字段未赋值,导致 b.Runner 是 nil,一调就 panic。
- 在
BaseProcessor.Execute()开头加防御性检查:if b.Runner == nil { return errors.New("Runner not set") } - 别在
BaseProcessor的构造函数里自动 new 一个默认实现——那会掩盖用户忘记设置的问题 - 如果某步需要共享状态(如中间数据),不要靠全局变量或闭包;把状态作为字段放在实现 struct 里,由
Runner自己管理 - 并发安全需额外考虑:
BaseProcessor本身不保存状态,但Runner实现若含字段,就要自己处理锁或用 sync.Pool
真正的难点不在写法,而在设计接口边界:哪些该进接口,哪些该作为参数传入,哪些该由调用方预处理。这得看具体场景,没法套模板。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










