模板方法模式属于行为型设计模式,它的目的是在一个方法中定义算法的骨架,而将某些步骤的具体实现延迟到子类中完成。在传统的面向对象语言如Java中,我们通常通过抽象类与继承来实现这一模式。但在Golang中并没有类和继承的概念,我们更多使用结构体嵌入和接口组合来达成同样的效果。这种方式不仅符合Go语言简洁直观的风格,也避免了继承带来的强耦合问题。

一、模板方法模式的核心思想
模板方法模式的关键在于“骨架固定,细节可变”。也就是说,一个完整的业务流程中,步骤的顺序、前置条件、后置处理往往是稳定的,但其中某几个环节会因为业务场景不同而有所变化。如果我们把整个流程写死在一个函数里,那么每支持一种新的变化,就要复制一遍流程代码,既容易出错也不利于维护。
在Golang里,我们可以定义一个基础结构体,它持有一个接口类型的字段,这个接口描述了那些需要延迟实现的步骤。基础结构体提供一个公开方法作为模板方法,该方法内部按照固定顺序调用自身步骤以及接口中的方法。具体的业务结构体嵌入基础结构体,并为接口提供自己的实现。这样一来,算法骨架由基础结构体控制,具体行为由业务结构体注入,实现了逻辑复用与扩展解耦。
二、使用结构体与接口实现模板方法
下面以“报告生成”为例。无论生成什么格式的报告,流程都是:准备数据、格式化数据、输出结果。其中格式化的方式可能是JSON、CSV或XML。我们用模板方法模式把前和后的步骤固定,把格式化延迟实现。
首先定义步骤接口和基础模板结构体:
package main
import "fmt"
// Formatter 定义需要延迟实现的格式化步骤
type Formatter interface {
Format(data map[string]string) string
}
// ReportTemplate 定义算法骨架的基础结构体
type ReportTemplate struct {
formatter Formatter
}
// Generate 是模板方法,固定了算法步骤
func (r *ReportTemplate) Generate(data map[string]string) {
fmt.Println("准备数据...")
content := r.formatter.Format(data)
fmt.Println("输出结果:", content)
}
// SetFormatter 注入具体实现
func (r *ReportTemplate) SetFormatter(f Formatter) {
r.formatter = f
}
上面的代码中,Generate方法就是模板方法,它先执行固定的准备动作,然后调用formatter.Format这个延迟实现的步骤,最后做输出。注意ReportTemplate本身并不关心格式化细节,只依赖Formatter接口。
接着我们实现具体的JSON和CSV格式化器,并嵌入模板来使用:
package main
import (
"encoding/json"
"strings"
)
// JSONFormatter 具体实现
type JSONFormatter struct{}
func (j *JSONFormatter) Format(data map[string]string) string {
b, _ := json.Marshal(data)
return string(b)
}
// CSVFormatter 具体实现
type CSVFormatter struct{}
func (c *CSVFormatter) Format(data map[string]string) string {
parts := []string{}
for k, v := range data {
parts = append(parts, k+":"+v)
}
return strings.Join(parts, ",")
}
func main() {
data := map[string]string{"name": "张三", "age": "30"}
tpl := &ReportTemplate{}
tpl.SetFormatter(&JSONFormatter{})
tpl.Generate(data)
tpl.SetFormatter(&CSVFormatter{})
tpl.Generate(data)
}
运行该程序会先以JSON形式输出,再以CSV形式输出,但两次调用都复用了同一套准备与输出逻辑。如果以后要增加XML格式,只需新增一个实现了Formatter接口的结构体,完全不用修改ReportTemplate。
三、与继承实现的对比及Go中的优势
在支持继承的语言中,模板方法通常依赖父类定义抽象方法,子类重写。这种方式会让子类与父类产生编译期绑定,父类protected方法的变化可能影响所有子类。而Go用组合方式,将可变部分抽成接口,运行时注入,更加灵活。比如同一个模板实例,可以在不同请求中切换不同实现,而不必为每个实现建一个子类类型。
另外,Go的模板方法实现更容易做单元测试。我们可以传入一个mock的Formatter来只验证模板方法的流程是否正确,而不依赖真实的格式化逻辑。从架构上看,这也符合“依赖倒置”原则:高层模块不依赖低层模块,二者都依赖抽象。
四、实际应用中的注意事项
虽然模板方法模式好用,但也不要盲目抽象。如果算法步骤本来就很短且几乎不会变化,硬拆出接口反而增加阅读成本。只有当“固定流程”和“多变步骤”的边界清晰,且多变步骤确实有多种实现时,才值得引入该模式。
在Go项目中,还可以结合函数选项模式或中间件思想,在模板方法前后统一加入日志、熔断、耗时统计等横切逻辑。例如把Generate改造成接受装饰函数,这样既保留了骨架稳定性,又扩展了非业务功能,整体设计会更加优雅和实用。