Go中模板方法用接口定义步骤契约+普通函数封装流程,如Processor接口与Run函数;钩子函数作字段需判空;所有钩子须支持context.Context;禁用embed模拟继承,避免反射与自动注册。

Go 里没有抽象类和继承,所以不能照搬 Java 的模板方法写法;真正管用的,是用接口定义步骤契约 + 普通函数封装固定流程。
用 interface + Run 函数实现最简骨架
这是最常用、最符合 Go 习惯的做法:不模拟“父类”,只明确“哪些步骤必须做、按什么顺序做”。
- 定义一个
Processor接口,每个方法对应一个可变步骤(如Validate()、Persist()) - 写一个普通函数
Run(p Processor) error,按序调用这些方法 - 具体业务结构体实现该接口,内部可自由持有
*sql.DB、context.Context等共享状态 - 别在接口里塞太多方法——5 个以上就该拆,比如把通知逻辑单独抽成
Notifier接口
type Processor interface {
Validate() error
Transform() error
Persist() error
}
func Run(p Processor) error {
if err := p.Validate(); err != nil {
return err
}
if err := p.Transform(); err != nil {
return err
}
return p.Persist()
}
用结构体字段存钩子函数,适合轻量或临时定制
当只有 1–3 个可变点,且不想定义新类型时,直接把函数设为结构体字段更直观。
- 字段名要体现语义和强制性,比如
Validate func() error是必填,Notify func() error可为 nil - 在
Execute()方法里显式判空,避免静默跳过关键步骤 - 测试时直接传入闭包,不用构造完整 struct,例如
pp.Validate = func() error { return nil } - 错误:别在
NewXXX()里偷偷给字段赋默认函数,这会让调用方误以为流程已完备
type PaymentProcessor struct {
Validate func() error
Charge func() error
Notify func() error
}
func (p *PaymentProcessor) Execute() error {
if p.Validate == nil {
return fmt.Errorf("Validate not set")
}
if err := p.Validate(); err != nil {
return err
}
// ...
}
所有钩子必须接收 context.Context 并支持中断
真实业务中,一个慢钩子(比如发微信通知)会拖垮整个流程;不加上下文管控,等于放弃超时和取消能力。
模板采用响应式设计,自动适应手机,电脑及平板显示;满足单一店铺外卖需求。功能:1.菜单分类管理2.菜品管理:菜品增加,删除,修改3.订单管理4.友情链接管理5.数据库备份6.文章模块:如:促销活动,帮助中心7.单页模块:如:企业信息,关于我们更强大的功能在开发中……安装方法:上传到网站根目录,运行http://www.***.com/install 自动
立即学习“go语言免费学习笔记(深入)”;
- 钩子方法签名统一为
func(ctx context.Context) error,主流程在每次调用前传入同一份ctx - 若某步需限流,用
ctx, cancel := context.WithTimeout(ctx, 3*time.Second)包一层再传 - 命名要带时序词,如
BeforePersist()、AfterSave(),避免逻辑错位(比如在AfterSave()里改数据库字段) - 别让钩子互相依赖私有字段传值——它们看到的应是同一份输入对象(如
*Order)和上下文
别用 embed 模拟继承,那是陷阱
有人试图嵌入一个“基类”结构体,再覆盖同名方法,但 Go 不支持方法重写:调用 p.Transform() 仍会走到嵌入字段的方法里,除非你手动重定向。
- 嵌入只提升字段访问,不改变方法绑定目标
- 结果是钩子被跳过、日志没打、事务没提交,问题极难定位
- 正确做法是:所有可变步骤都声明为接口方法,由 concrete struct 显式实现
- 如果真需要复用逻辑(如通用日志),抽成独立函数,再在具体实现里调用,而不是塞进“父结构体”
最容易被忽略的一点是:模板方法不是为了“看起来像 OOP”,而是为了确保流程顺序不可篡改、每一步意图清晰可验证。一旦开始用反射、类型断言或 init 自动注册,你就已经偏离了 Go 的设计哲学。









