如何使用Golang实现模板方法模式_Golang模板方法模式设计与优化

5次阅读

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

如何使用Golang实现模板方法模式_Golang模板方法模式设计与优化

模板方法的核心在于抽象流程,不是写死逻辑

Go 没有继承和抽象类,所以不能照搬 java 那套 abstract class + final method 的写法。强行用嵌入结构体 + 空接口模拟,反而让调用链变深、类型信息丢失、ide 支持变差。真正该做的是:用函数值(func())或接口定义「可插拔的步骤」,把不变的流程控制权收在主函数里。

比如一个导出报告的流程:加载数据 → 格式化 → 写入文件。其中「加载数据」和「写入文件」因来源/目标不同而变化,但「先加载、再格式化、最后写入」这个顺序永远不变——这个顺序就是模板方法要封装的东西。

用函数字段替代子类重写,避免接口爆炸

常见错误是为每个可变步骤定义单独接口,比如 DataLoaderformatterWriter,然后组合进一个结构体。这会导致接口数量膨胀,且使用者必须实现所有方法,哪怕只改一个步骤。

更轻量的做法是直接把步骤声明为结构体字段,类型为函数:

立即学习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}

不需要定义新类型,也不用实现一空方法。

当步骤需要共享状态时,用闭包比传参更干净

如果多个步骤需共用配置、缓存或连接对象(比如同一个 *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 连接)必须明确谁负责关闭——通常应由函数内部处理,不要指望模板方法统一回收

最容易被忽略的是:闭包捕获的变量生命周期可能超出预期。比如在循环中为多个生成器赋值函数,却引用了循环变量,结果所有函数都绑定到最后一次迭代的值。

text=ZqhQzanResources