

新闻资讯
技术学院模板方法应使用 interface + struct 组合实现,主流程固定、钩子由 interface 定义并由具体 struct 实现,所有钩子需接收 context.Context 参数,命名体现时序,返回 error 以支持中断,测试用匿名 struct 验证调用顺序。
Go 没有类继承,所谓“模板方法”本质是定义一组固定执行顺序的公共逻辑,把可变部分抽成 interface 方法,由具体结构体实现。关键不是“复用父类”,而是“控制流程骨架,开放扩展点”。
常见错误是试图用嵌入(embedding)模拟继承,结果导致方法调用链混乱、钩子被跳过。正确做法是:公共流程写在普通函数或某个 struct 的方法里,所有可变步骤都声明为 interface 方法,再由 concrete struct 实现。
interface 只定义钩子方法名和签名,不暴露实现细节interface 作为参数,而非依赖具体类型业务流程中钩子不是越多越好,而是要明确它在哪个环节介入。比如订单创建流程: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 参数测模板流程是否按序调用钩子,不需要启动 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。