Golang策略模式替代大量if else判断

策略模式优于if else因其支持开闭原则、便于测试和扩展;通过统一接口、注册表和安全执行机制实现解耦,避免硬编码、类型不安全及并发问题。

为什么用策略模式而不是 if else

当业务逻辑分支超过 3–4 个、且每个分支的处理逻辑差异明显(比如调用不同 API、走不同数据库表、生成不同结构 JSON),硬写 if else 会导致函数越来越长、难以测试、新增类型要改旧代码——违反开闭原则。策略模式把每个分支封装成独立类型,运行时靠键(如字符串)查表选实现,新增策略不碰原有逻辑。

如何定义策略接口和注册表

核心是统一接口 + 全局可扩展的映射。避免用 map[string]interface{} 强转,直接用接口类型做值;注册过程建议放在 init() 或显式 Register() 函数里,防止漏注册导致 panic。

  • 接口方法参数尽量精简,只传必要上下文(比如 *Ordermap[string]any),避免每加一个策略就改接口
  • 注册键名统一小写或加前缀(如 "pay_alipay"),避免大小写混淆
  • 查策略时用 sync.Map 或加读锁的普通 map,高并发下别裸用非线程安全 map
type PaymentStrategy interface {
    Process(ctx context.Context, order *Order) error
}

var strategies = make(map[string]PaymentStrategy)

func Register(name string, s PaymentStrategy) {
    strategies[name] = s
}

func GetStrategy(name string) (PaymentStrategy, bool) {
    s, ok := strategies[name]
    return s, ok
}

运行时如何安全选择并执行策略

不能假设键一定存在。生产环境必须处理未注册策略的兜底逻辑(比如返回错误、打日志、走默认策略),否则 panic: assignment to entry in nil map 或空指针解引用极易发生。

  • 不要在 switch 中硬编码所有策略名——那又回到 if else 的老路
  • 策略执行失败时,错误要带策略名(fmt.Errorf("alipay strategy failed: %w", err)),方便排查是哪个策略出问题
  • 如果策略需初始化(如依赖 HTTP client、DB 连接),应在 Register() 前完成,别拖到 Process() 里懒加载
func HandlePayment(ctx context.Context, method string, order *Order) error {
    strategy, ok := GetStrategy(method)
    if !ok {
        return fmt.Errorf("unknown payment method: %s", method)
    }
    return strategy.Process(ctx, order)
}

常见踩坑:泛型策略与配置驱动的陷阱

Go 1.18+ 虽支持泛型,但策略接口本身不宜过度泛型化。比如定义 type Strategy[T any] interface { Execute(T) error },会导致注册表变成 map[string]interface{},丧失类型安全。更稳妥的做法是让具体策略内部处理类型转换,接口保持稳定。

  • 配置文件(如 YAML)里写的策略名,必须和代码中 Register() 的键完全一致——建议写单元测试校验所有配置键是否已注册
  • 别把策略实现写在 HTTP handler 里,它应该无 HTTP 概念,只专注领域逻辑
  • 单元测试每个策略时,用接口 mock,不要启动真实支付网关

最易被忽略的一点:策略之间如果有共享状态(比如共用一个 redis 连接池),别在每个策略里重复初始化,应通过依赖注入或全局变量统一管理。