如何在Golang中实现模板方法与钩子结合_Golang模板方法业务流程示例

18次阅读

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

如何在Golang中实现模板方法与钩子结合_Golang模板方法业务流程示例

模板方法必须用 interface + struct 组合,不能只靠继承

go 没有类继承,所谓“模板方法”本质是定义一组固定执行顺序的公共逻辑,把可变部分抽成 interface 方法,由具体结构体实现。关键不是“复用父类”,而是“控制流程骨架,开放扩展点”。

常见错误是试图用嵌入(embedding)模拟继承,结果导致方法调用链混乱、钩子被跳过。正确做法是:公共流程写在普通函数或某个 struct 的方法里,所有可变步骤都声明为 interface 方法,再由 concrete struct 实现。

  • interface 只定义钩子方法名和签名,不暴露实现细节
  • 模板主流程函数接收该 interface 作为参数,而非依赖具体类型
  • 避免在模板函数内部做类型断言或反射——破坏契约,也难测试

钩子命名要体现执行时机,比如 BeforeValidate / AfterSave

业务流程中钩子不是越多越好,而是要明确它在哪个环节介入。比如订单创建流程:BeforeCreateValidateBeforePersistAfterSaveNotify。名字带时序词,能立刻看出是否遗漏、是否错位。

容易踩的坑是把校验逻辑塞进 BeforeSave,结果事务已开启却抛错回滚困难;或者在 AfterSave 里改数据库字段,违反“保存后不可变”契约。

立即学习go语言免费学习笔记(深入)”;

  • 钩子方法应是无副作用的纯函数,或明确标注其副作用(如发消息、写日志)
  • 如果某个钩子可能失败且需中断流程,它的返回值必须是 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。

text=ZqhQzanResources