模板方法模式在Go中通过函数字段或接口实现,核心是控制反转:结构体定义固定流程,用户注入钩子逻辑;函数字段轻量适合简单场景,接口更易测试和共享状态,需注意错误处理、资源清理与并发安全。

模板方法模式在 Go 中没有语言级支持,但能用函数值和接口实现
Go 没有抽象类或 abstract 方法,所以不能像 Java 那样直接定义“子类必须重写”的钩子。但你可以用组合 + 接口 + 函数字段模拟出等效行为:把算法骨架封装在结构体里,把可变步骤暴露为 func() 或接口方法。
关键不是“继承复用”,而是“控制反转”——主流程由模板结构体控制,细节由使用者注入。
- 核心结构体定义固定执行顺序(例如
Execute()) - 通过字段(如
before, process, after func())或嵌入接口(如Processor)让调用方提供定制逻辑 - 所有钩子函数默认可为空(nil-safe),避免 panic
用函数字段实现可插拔的模板流程
这是最轻量、最符合 Go 习惯的做法。适合流程简单、变化点少(通常 ≤3 个)的场景。
type DataProcessor struct {
before func()
process func() error
after func()
}
func (dp *DataProcessor) Execute() error {
if dp.before != nil {
dp.before()
}
if dp.process == nil {
return fmt.Errorf("process function not set")
}
if err := dp.process(); err != nil {
return err
}
if dp.after != nil {
dp.after()
}
return nil
}
使用时直接赋值函数:
立即学习“go语言免费学习笔记(深入)”;
动态WEB网站中的PHP和MySQL详细反映实际程序的需求,仔细地探讨外部数据的验证(例如信用卡卡号的格式)、用户登录以及如何使用模板建立网页的标准外观。动态WEB网站中的PHP和MySQL的内容不仅仅是这些。书中还提到如何串联JavaScript与PHP让用户操作时更快、更方便。还有正确处理用户输入错误的方法,让网站看起来更专业。另外还引入大量来自PEAR外挂函数库的强大功能,对常用的、强大的包
proc := &DataProcessor{
before: func() { log.Println("starting...") },
process: func() error {
return os.WriteFile("output.txt", []byte("hello"), 0644)
},
after: func() { log.Println("done.") },
}
err := proc.Execute()
- 函数字段天然支持闭包,可捕获上下文变量(比如配置、数据库连接)
- 不强制实现接口,调用方零依赖
- 注意检查
nil——尤其process这类必填项,漏设会导致运行时报错
用接口替代函数字段提升可测试性与扩展性
当流程变复杂、需要 mock、或者多个步骤之间要共享状态(如中间结果缓存)时,接口更合适。定义一个最小接口,让使用者实现它:
type Processor interface {
Setup() error
Run() error
Cleanup() error
}
type TemplateRunner struct {
p Processor
}
func (tr *TemplateRunner) Execute() error {
if err := tr.p.Setup(); err != nil {
return err
}
if err := tr.p.Run(); err != nil {
return err
}
return tr.p.Cleanup()
}
实现示例:
type FileJob struct {
InputPath string
OutputPath string
Content []byte
}
func (f *FileJob) Setup() error {
if _, err := os.Stat(f.InputPath); os.IsNotExist(err) {
return fmt.Errorf("input file missing: %s", f.InputPath)
}
return nil
}
func (f *FileJob) Run() error {
return os.WriteFile(f.OutputPath, f.Content, 0644)
}
func (f *FileJob) Cleanup() error {
log.Printf("wrote %d bytes to %s", len(f.Content), f.OutputPath)
return nil
}
// 使用
runner := &TemplateRunner{p: &FileJob{
InputPath: "config.yaml",
OutputPath: "result.json",
Content: []byte(`{"status":"ok"}`),
}}
runner.Execute()
- 接口使单元测试容易:可传入 fake 实现,验证各阶段是否被调用
- 方法间可共享字段(如
FileJob的Content),避免重复参数传递 - 别忘了导出接口方法名(首字母大写),否则外部包无法实现
常见陷阱:延迟执行、错误传播和生命周期管理
模板方法看似简单,但在真实项目中容易因忽略执行时机或错误处理导致静默失败或资源泄漏。
-
defer在模板方法内部使用要谨慎:如果Setup()失败,defer Cleanup()不会触发;应改用显式if err != nil { p.Cleanup() } - 错误不应被吞掉:每个钩子返回的
error都该被检查并按需中断流程,不要只写_ = p.Run() - 如果钩子涉及打开文件、网络连接或 goroutine,确保
Cleanup()或after真正释放资源——Go 不会自动帮你关文件描述符 - 并发安全不默认成立:若多个 goroutine 共享同一个
TemplateRunner实例,且其内部状态(如计数器、缓存 map)被修改,需加锁
真正难的不是写骨架,而是想清楚哪些步骤必须串行、哪些可选、错误发生时要不要回滚、以及谁负责清理。这些决策不会出现在代码结构里,但会决定系统是否健壮。









