如何使用Golang实现策略与上下文模式_Golang策略模式动态切换示例

策略接口应优先用泛型(Go 1.18+),如 Strategy[T any];无状态策略可值接收,有状态必须指针接收;运行时切换需 sync.RWMutex 保护;注册表宜封装而非裸 map。

策略接口定义必须用 interface{} 还是具体类型?

Go 没有泛型约束前(Go 1.18 之前),策略接口通常用 interface{} 接收任意输入,但这会丢失类型安全、增加运行时断言风险。Go 1.18+ 强烈建议用泛型接口,避免类型转换错误。

  • 旧写法:type Strategy interface { Execute(data interface{}) error } → 调用方需手动 data.(string),易 panic
  • 推荐写法:type Strategy[T any] interface { Execute(data T) error } → 编译期检查输入类型
  • 若策略只处理一种固定类型(如 float64),直接定义为 type DiscountStrategy interface { Apply(price float64) float64 } 更清晰

Context 结构体要不要持有策略实例的指针?

要,但取决于策略是否带状态。无状态策略(如纯函数式计算)可传值;有状态策略(如带计数器、缓存)必须传指针,否则每次调用都是副本,状态不累积。

type Context struct {
    strategy Strategy[float64] // 接口类型,值接收即可
}

// ✅ 正确:策略实现本身无内部字段,或字段不需跨调用共享 type FlatRateDiscount struct{} func (f FlatRateDiscount) Apply(price float64) float64 { return price * 0.9 }

// ❌ 错误:若策略含计数器,用值接收会导致每次调用都重置 count type CountedDiscount struct { count int } func (c CountedDiscount) Apply(price float64) float64 { // 值接收 → c 是副本 c.count++ // 不影响原实例 return price 0.95 } // ✅ 应改为指针接收:func (c CountedDiscount) Apply(price float64) float64

如何在运行时安全切换策略而不引发竞态?

Context 中的策略字段若被多 goroutine 并发读写(如管理员动态更新折扣策略),必须加锁。不要依赖“只读切换”假设 —— Go 编译器不保证字段读写的原子性。

  • sync.RWMutex:读多写少场景下,RLock()Execute() 使用,Lock() 仅用于 SetStrategy()
  • 避免在策略方法内调用 Context.SetStrategy(),容易死锁
  • 切换后可加简单校验,比如调用 strategy.(fmt.Stringer).String() 确保非 nil
type Context struct {
    mu        sync.RWMutex
    strategy  Strategy[float64]
}

func (c *Context) Execute(price float64) float64 { c.mu.RLock() defer c.mu.RUnlock() if c.strategy == nil { return price // 或 panic / 返回错误 } return c.strategy.Apply(price) }

func (c *Context) SetStrategy(s Strategy[float64]) { c.mu.Lock() defer c.mu.Unlock() c.strategy = s }

为什么不用 map[string]Strategy 直接做策略注册表?

可以,但要注意 key 冲突、类型擦除和初始化顺序问题。纯 map 注册缺少编译期校验,运行时取错 key 会 panic;更健壮的做法是封装成带方法的注册器。

  • 别直接写:strategies := map[string]Strategy[float64]{ "vip": VIPDiscount{} } → key 拼错无提示
  • 推荐封装:reg := NewStrategyRegistry(); reg.Register("vip", VIPDiscount{}),内部用 map + 类型断言 + 重复注册检测
  • 若策略需依赖注入(如 DB client),map[string]interface{} + reflect 调用太重,应改用函数工厂:func() Strategy[float64] { return &DBAwareDiscount{db: db} }

策略切换本身不复杂,真正容易出问题的是状态共享边界和并发控制粒度 —— 多数线上 bug 都来自策略实例被意外复用或未加锁覆盖。