模板方法应使用 interface + struct 组合实现,主流程固定、钩子由 interface 定义并由具体 struct 实现,所有钩子需接收 context.Context 参数,命名体现时序,返回 error 以支持中断,测试用匿名 struct 验证调用顺序。

模板方法必须用 interface + struct 组合,不能只靠继承
Go 没有类继承,所谓“模板方法”本质是定义一组固定执行顺序的公共逻辑,把可变部分抽成 interface 方法,由具体结构体实现。关键不是“复用父类”,而是“控制流程骨架,开放扩展点”。
常见错误是试图用嵌入(embedding)模拟继承,结果导致方法调用链混乱、钩子被跳过。正确做法是:公共流程写在普通函数或某个 struct 的方法里,所有可变步骤都声明为 interface 方法,再由 concrete struct 实现。
-
interface只定义钩子方法名和签名,不暴露实现细节 - 模板主流程函数接收该
interface作为参数,而非依赖具体类型 - 避免在模板函数内部做类型断言或反射——破坏契约,也难测试
钩子命名要体现执行时机,比如 BeforeValidate / AfterSave
业务流程中钩子不是越多越好,而是要明确它在哪个环节介入。比如订单创建流程:BeforeCreate、Validate、BeforePersist、AfterSave、Notify。名字带时序词,能立刻看出是否遗漏、是否错位。
容易踩的坑是把校验逻辑塞进 BeforeSave,结果事务已开启却抛错回滚困难;或者在 AfterSave 里改数据库字段,违反“保存后不可变”契约。
- 钩子方法应是无副作用的纯函数,或明确标注其副作用(如发消息、写日志)
- 如果某个钩子可能失败且需中断流程,它的返回值必须是
error,模板主流程要检查并处理 - 不要让钩子互相依赖状态——每个钩子看到的应是同一份输入上下文(如
*Order),而不是靠私有字段传值
用 context.Context 控制超时与取消,别让钩子拖垮整个流程
真实业务中,Notify 钩子调用微信接口可能卡住,Validate 调外部风控服务可能慢。模板方法主流程若不统一管控上下文,一个慢钩子会让整个订单创建耗时飙升。
正确做法是在模板入口接收 context.Context,并在每个钩子调用前传入(必要时用 ctx, cancel := context.WithTimeout(...) 限流)。
func (s *OrderService) CreateOrder(ctx context.Context, order *Order) error {
if err := s.beforeCreate(ctx, order); err != nil {
return err
}
if err := s.validate(ctx, order); err != nil {
return err
}
// ...
return nil
}
- 所有钩子方法签名必须包含
ctx context.Context参数 - 不要在钩子里起 goroutine 并忽略 ctx —— 这会导致泄漏和超时失效
- 日志、监控埋点也要基于同一份 ctx,才能串联 trace
测试钩子组合时,用匿名 struct 实现 interface,别 mock 整个 service
测模板流程是否按序调用钩子,不需要启动 DB 或 HTTP server。最轻量方式是构造一个满足 interface 的匿名 struct,每个钩子方法只打日志或设标志位,然后断言调用顺序。
type mockOrderProcessor struct {
calls []string
}
func (m *mockOrderProcessor) BeforeCreate(ctx context.Context, o *Order) error {
m.calls = append(m.calls, "BeforeCreate")
return nil
}
func (m *mockOrderProcessor) Validate(ctx context.Context, o *Order) error {
m.calls = append(m.calls, "Validate")
return nil
}
// ... 其他钩子
func TestCreateOrder_CallsHooksInOrder(t *testing.T) {
m := &mockOrderProcessor{}
err := CreateOrder(context.Background(), &Order{}, m)
assert.NoError(t, err)
assert.Equal(t, []string{"BeforeCreate", "Validate", "BeforePersist", "AfterSave"}, m.calls)
}
复杂点在于:有些钩子依赖外部状态(如库存服务返回 false 导致 Validate 失败),这时只需在对应方法里 return error,不用模拟整套依赖。真要集成测试,再单独写 case。










