
本文介绍如何在 go 中以简洁、安全、符合惯用法的方式实现模板方法模式,通过函数注入或接口组合替代面向对象语言中的虚函数机制,避免空指针错误并保持逻辑清晰。
本文介绍如何在 go 中以简洁、安全、符合惯用法的方式实现模板方法模式,通过函数注入或接口组合替代面向对象语言中的虚函数机制,避免空指针错误并保持逻辑清晰。
模板方法模式的核心在于定义一个算法骨架(如 Start),将可变行为(如 Run)延迟到子类实现。Go 没有继承和虚函数,但可通过组合 + 函数值注入或接口显式实现两种主流方式优雅达成相同效果,且更轻量、更可控。
✅ 推荐方式一:函数字段注入(简洁直接)
这是最符合 Go 惯用法的实现——将具体行为作为 func() 类型字段嵌入结构体,由构造函数完成绑定:
package main
import "fmt"
type Runner struct {
run func() // 模板方法中被调用的“钩子”
}
// Start 是不变的算法骨架:准备 → 执行核心逻辑
func (r *Runner) Start() {
fmt.Println("Preparing...")
if r.run != nil {
r.run()
} else {
panic("runner.run not implemented")
}
fmt.Println("Done.")
}
// 具体实现只需提供函数,无需额外结构体
func runLogger() {
fmt.Println("Running logger task...")
}
func runExporter() {
fmt.Println("Exporting data...")
}
func main() {
logger := &Runner{run: runLogger}
exporter := &Runner{run: runExporter}
logger.Start() // 输出:Preparing... → Running logger task... → Done.
exporter.Start() // 输出:Preparing... → Exporting data... → Done.
}
✅ 优势:零内存开销、类型安全、无 nil 解引用风险(可加非空校验)、易于测试(可传入 mock 函数)。
✅ 推荐方式二:接口组合(支持状态与多方法)
当子类需维护自身状态(如 Logger.i int)或多步骤回调时,推荐定义明确接口并组合:
type Runner interface {
Run() // 模板方法调用的抽象行为
}
type BaseRunner struct {
runner Runner
}
func (br *BaseRunner) Start() {
fmt.Println("Initializing...")
br.runner.Run() // 委托给具体实现
fmt.Println("Cleanup completed.")
}
// 具体实现类型(含状态)
type Logger struct {
i int
}
func (l *Logger) Run() {
l.i++
fmt.Printf("Logger running, count=%d\n", l.i)
}
func NewLogger() *BaseRunner {
return &BaseRunner{runner: &Logger{i: 0}}
}
func main() {
l := NewLogger()
l.Start() // Initializing... → Logger running, count=1 → Cleanup completed.
l.Start() // count=2(状态保留)
}
✅ 优势:天然支持状态管理、便于扩展(如添加 BeforeRun()/AfterRun())、符合 Go 接口设计哲学。
⚠️ 注意事项与避坑指南
- 避免空指针:原始代码中 l.doRun = l.doRunImpl 在 l 尚未完全初始化时赋值,导致 l 为 nil;函数注入应在结构体创建后立即完成(如构造函数内),或直接使用闭包捕获实例。
- 不要滥用嵌入:type Logger struct { *Runner } 并不能解决“基类访问子类方法”的问题(Go 不支持虚调用),反而引入歧义和生命周期风险。
- 并发安全:若 Start() 启动 goroutine,请确保 run 函数内状态访问是线程安全的(如加锁或使用 channel)。
- 可测试性优先:函数字段或接口方式均便于单元测试——可轻松注入 stub 或 mock 实现。
总之,Go 中的模板方法模式不靠继承,而靠组合、函数式编程与接口契约。选择函数注入还是接口实现,取决于是否需要状态隔离与行为复用粒度——二者皆简洁、安全、地道。











