模板方法在 Go 中应以函数字段替代抽象类,封装固定流程(如加载→格式化→写入),通过闭包共享状态并注意并发安全与资源生命周期。

模板方法的核心在于抽象流程,不是写死逻辑
Go 没有继承和抽象类,所以不能照搬 Java 那套 abstract class + final method 的写法。强行用嵌入结构体 + 空接口模拟,反而让调用链变深、类型信息丢失、IDE 支持变差。真正该做的是:用函数值(func())或接口定义「可插拔的步骤」,把不变的流程控制权收在主函数里。
比如一个导出报告的流程:加载数据 → 格式化 → 写入文件。其中「加载数据」和「写入文件」因来源/目标不同而变化,但「先加载、再格式化、最后写入」这个顺序永远不变——这个顺序就是模板方法要封装的东西。
用函数字段替代子类重写,避免接口爆炸
常见错误是为每个可变步骤定义单独接口,比如 DataLoader、Formatter、Writer,然后组合进一个结构体。这会导致接口数量膨胀,且使用者必须实现所有方法,哪怕只改一个步骤。
更轻量的做法是直接把步骤声明为结构体字段,类型为函数:
立即学习“go语言免费学习笔记(深入)”;
type ReportGenerator struct {
LoadData func() ([]byte, error)
Format func([]byte) ([]byte, error)
Write func([]byte) error
}
func (g *ReportGenerator) Execute() error {
data, err := g.LoadData()
if err != nil {
return err
}
formatted, err := g.Format(data)
if err != nil {
return err
}
return g.Write(formatted)
}
使用时只需赋值关心的函数:
gen := &ReportGenerator{LoadData: fetchFromDB, Format: json.Marshal, Write: writeFile}
不需要定义新类型,也不用实现一堆空方法。
使用模板与程序分离的方式构建,依靠专门设计的数据库操作类实现数据库存取,具有专有错误处理模块,通过 Email 实时报告数据库错误,除具有满足购物需要的全部功能外,成新商城购物系统还对购物系统体系做了丰富的扩展,全新设计的搜索功能,自定义成新商城购物系统代码功能代码已经全面优化,杜绝SQL注入漏洞前台测试用户名:admin密码:admin888后台管理员名:admin密码:admin888
当步骤需要共享状态时,用闭包比传参更干净
如果多个步骤需共用配置、缓存或连接对象(比如同一个 *sql.DB),别在每个函数签名里重复加参数。用闭包捕获环境变量,保持函数签名简洁:
错误示范(签名冗长,易漏传):func loadFromDB(db *sql.DB, query string) ([]byte, error)func writeToS3(s3Client *s3.Client, bucket, key string, data []byte) error
正确做法(闭包封装依赖):
db := connectDB()
gen.LoadData = func() ([]byte, error) {
return loadFromDB(db, "SELECT ...")
}
s3Client := newS3Client()
gen.Write = func(data []byte) error {
return writeToS3(s3Client, "my-bucket", "report.json", data)
}
这样既避免参数污染函数契约,又让依赖关系显式可读。
注意并发安全与生命周期管理
模板方法本身不保证线程安全。如果 ReportGenerator 实例被多个 goroutine 共享,而它的字段(如 LoadData)内部用了非并发安全的资源(如未加锁的 map、复用的 bytes.Buffer),就会出问题。
关键判断点:
- 若生成器实例是短命的(每次请求新建一个),字段函数可自由使用局部资源
- 若复用实例(如全局单例),所有字段函数必须自身线程安全,或对共享状态加锁
- 字段函数中打开的资源(如文件句柄、HTTP 连接)必须明确谁负责关闭——通常应由函数内部处理,不要指望模板方法统一回收
最容易被忽略的是:闭包捕获的变量生命周期可能超出预期。比如在循环中为多个生成器赋值函数,却引用了循环变量,结果所有函数都绑定到最后一次迭代的值。









