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

为什么用策略模式而不是 if else
当业务逻辑分支超过 3–4 个、且每个分支的处理逻辑差异明显(比如调用不同 API、走不同数据库表、生成不同结构 JSON),硬写 if else 会导致函数越来越长、难以测试、新增类型要改旧代码——违反开闭原则。策略模式把每个分支封装成独立类型,运行时靠键(如字符串)查表选实现,新增策略不碰原有逻辑。
如何定义策略接口和注册表
核心是统一接口 + 全局可扩展的映射。避免用 map[string]interface{} 强转,直接用接口类型做值;注册过程建议放在 init() 或显式 Register() 函数里,防止漏注册导致 panic。
- 接口方法参数尽量精简,只传必要上下文(比如
*Order或map[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{},丧失类型安全。更稳妥的做法是让具体策略内部处理类型转换,接口保持稳定。
立即学习“go语言免费学习笔记(深入)”;
-
配置文件(如 YAML)里写的策略名,必须和代码中
Register()的键完全一致——建议写单元测试校验所有配置键是否已注册 - 别把策略实现写在 HTTP handler 里,它应该无 HTTP 概念,只专注领域逻辑
- 单元测试每个策略时,用接口 mock,不要启动真实支付网关
最易被忽略的一点:策略之间如果有共享状态(比如共用一个 redis 连接池),别在每个策略里重复初始化,应通过依赖注入或全局变量统一管理。










