go中用接口+组合+函数字段模拟模板方法模式:结构体封装固定流程,函数字段实现可变步骤和钩子,钩子需nil默认、调用前判空并recover防panic。

Go 里没有继承,怎么模拟模板方法模式
模板方法模式依赖父类定义骨架、子类实现细节,但 Go 没有类继承。实际做法是用接口 + 组合 + 函数字段来等效替代:把“算法骨架”写成结构体方法,把“可变步骤”声明为接口方法或函数类型字段,运行时注入具体行为。
关键不是模仿 OOP 形式,而是守住核心语义:固定流程控制权在主逻辑,变化点延迟到调用方决定。
常见错误是强行用嵌入(embedding)模拟继承,结果导致方法集爆炸、职责不清。正确姿势是明确区分「不可变的执行顺序」和「可插拔的行为契约」。
- 定义一个
Processor接口,只包含Execute()方法作为统一入口 - 用结构体(如
BaseProcessor)封装公共流程,内部字段如validateFn、transformFn、saveFn均为func() error类型 - 提供构造函数(如
NewBaseProcessor)接受这些函数,不暴露字段直接赋值 - 子流程不写死,而是调用字段函数,并在前后插入默认空实现(即钩子)
钩子函数该定义成方法还是字段
钩子本质是可选、可覆盖的扩展点,比如 BeforeSave 或 OnFailure。在 Go 中必须用函数字段(func() error 或 func(*Context)),不能用方法——因为方法属于类型,无法被调用方自由替换;而函数字段可 nil,默认不执行,也支持传入闭包捕获上下文。
性能上无差异,但语义清晰:钩子不是主体逻辑的一部分,只是监听点。如果误定义为结构体方法,会导致每次都要重写整个结构体,失去组合灵活性。
典型陷阱是把钩子设为非空默认函数(如打印日志),结果下游忘记显式设置就意外触发副作用。应始终默认为 nil,并在调用前判空:
if p.beforeSaveFn != nil {
if err := p.beforeSaveFn(); err != nil {
return err
}
}
如何避免钩子执行顺序混乱和 panic
多个钩子(如 BeforeProcess、AfterProcess、OnError)一旦调用顺序错乱或未处理 panic,整个流程就会中断。Go 没有 try/catch,必须手动 recover。
实操建议:
- 所有钩子调用都包裹在
defer func()+recover()中,转为返回 error(例如:fmt.Errorf("panic in BeforeProcess: %v", r)) - 钩子函数签名统一加
*Context参数,让调用方能读写共享状态,避免靠全局变量或闭包传递数据 - 禁止在钩子中修改主流程结构体字段(如重置
retryCount),改用 Context 字段透出 - 提供
WithHook链式构造器,而不是暴露字段直接赋值,防止并发写冲突
一个最小可运行的模板+钩子示例
下面是一个带钩子的处理器骨架,不依赖任何外部库:
type Context struct {
Data map[string]interface{}
}
type Processor struct {
validateFn func(*Context) error
processFn func(*Context) error
saveFn func(*Context) error
beforeSave func(*Context) error
afterSave func(*Context) error
onError func(*Context, error) error
}
func NewProcessor(v, p, s func(*Context) error) *Processor {
return &Processor{
validateFn: v,
processFn: p,
saveFn: s,
}
}
func (p *Processor) Execute(ctx *Context) error {
if p.validateFn != nil {
if err := p.validateFn(ctx); err != nil {
return p.callOnError(ctx, err)
}
}
if p.processFn != nil {
if err := p.processFn(ctx); err != nil {
return p.callOnError(ctx, err)
}
}
if p.beforeSave != nil {
if err := p.beforeSave(ctx); err != nil {
return p.callOnError(ctx, err)
}
}
if p.saveFn != nil {
if err := p.saveFn(ctx); err != nil {
return p.callOnError(ctx, err)
}
}
if p.afterSave != nil {
if err := p.afterSave(ctx); err != nil {
return p.callOnError(ctx, err)
}
}
return nil
}
func (p *Processor) callOnError(ctx *Context, err error) error {
if p.onError == nil {
return err
}
return p.onError(ctx, err)
}
使用时只需传入具体函数,钩子全可选。最易被忽略的是:钩子函数内部若发生 panic,会直接崩掉整个 Execute 流程——这正是为什么每个钩子调用前都得加 recover 包裹,而示例中省略了这部分,实际项目必须补上。











