大家好,我是十三!欢迎来到十三Tech。

最近在整理一份 Go 项目里促销模块的代码评审记录,看到一个 800 多行的 calculate.go——核心是一个长长的 switch,处理折扣、满减、返现、N 选 1、组合促销等十几种规则。每加一种促销,就在 switch 里添一个 case,文件越长越没人敢动。

这种代码每个老项目都有。大多数文章会告诉你:「用策略模式,消除 if-else」。但这句话其实是个结果,不是理由。真正该问的是——什么程度的分支才值得抽策略?2 个分支要不要抽?策略之间有依赖怎么办?

后来我把那块代码按策略 + map 注册重写了一遍,确实清爽,但也踩了几个坑。这一篇就来聊聊策略模式在 Go 工程里的真实代价和收益。

策略模式的现实类比:出行方式根据场景切换

一、策略模式在解决什么——不是「消除 if-else」

把教科书定义翻一下:策略模式把一组可互换的算法封装成对象,让客户端在运行时选择。注意关键词是可互换——不是「分散」,不是「独立」。

很多人误把策略模式当成"switch 重构工具",结果把一堆根本不可互换的分支也抽成了策略。比如订单状态机里 case pending / case paid / case shipped——这些状态之间有强语义顺序,根本不能互换,硬抽成策略反而让状态流转变得难以追踪。

真正的策略长这样:折扣计算的 8 折、满 100 减 20、返 5% 积分——这些算法互相独立、可以自由替换、客户端不关心你具体用哪个。这才是策略的地盘。

判断口诀记一下:客户端主动选算法 → 策略;对象自己根据内部状态变 → 状态模式;请求沿链传递 → 责任链。三者结构相似,意图完全不同。

二、Go 工程里最地道的写法:map 注册,不是 switch

教科书策略模式通常长这样——一个 Strategy 接口 + 一堆 ConcreteStrategy + 一个 Context 持有 Strategy 引用。Java 写多了的人会原样搬到 Go。

type Strategy interface {
    Calculate(o *Order) int
}

type Calculator struct{ s Strategy }
func (c *Calculator) Calc(o *Order) int { return c.s.Calculate(o) }

这段代码没问题,但直接搬到 Go 工程里你会觉得别扭——Context 这一层在大多数场景是多余的,因为它只是把调用转发给 Strategy。Go 社区更地道的写法是用 map 注册策略,让调用方按 key 取,彻底干掉 switch。

策略模式结构:Context 委托给 Strategy

下面这段是我项目里实际在用的写法。看 map 这一行就够了:

var strategies = map[string]Strategy{
    "discount": &DiscountStrategy{Rate: 0.8},
    "fullCut":  &FullCutStrategy{Threshold: 10000, Cut: 2000},
    "cashback": &CashbackStrategy{Rate: 0.05},
}

func CalcByType(o *Order, t string) int {
    if s, ok := strategies[t]; ok { return s.Calculate(o) }
    return o.Total()
}

对比一下原来那个 800 行 switch:加新促销规则只要在 map 里添一行,完全不动 CalcByType 函数。这才是策略模式在工程里真正的价值——不是「消除 if-else」这种表面功夫,而是让算法变化点收敛到一处

策略模式调用流程:客户端按 key 取策略后委托执行

策略本身怎么写?没什么神秘的,就是实现 Strategy 接口的普通结构体。DiscountStrategy 持有一个 Rate 字段,Calculate 方法把订单金额乘以折扣率返回。每个策略只关心自己那点计算逻辑,互相不知道彼此存在——这是策略模式能成立的前提。

三、什么时候别用——策略是有代价的

策略模式不是银弹,工程里至少有三个坑要知道。

坑一:只有 2 个分支且基本不变——直接 if-else 比 map 注册清晰得多。策略模式会引入接口、map、注册流程,对小问题来说这是过度设计。判断标准很简单:分支会不会持续增加? 不会就别抽。

坑二:策略之间有强依赖。比如 A 策略依赖 B 策略的输出,或者策略必须按特定顺序执行。策略模式假设策略独立,强依赖说明你需要的其实是责任链或模板方法。强行用策略,最后会变成「策略里调策略」的怪物。

坑三:策略逻辑相同、只是参数不同。比如 8 折、7 折、6 折——不要写三个 DiscountStrategy 实例然后注册成三个策略,这是把参数当算法。一个 DiscountStrategy 接受 Rate 参数就够了,策略的数量应该等于算法的种类,不是参数的组合。

策略适用决策:分支会扩 + 策略独立 → 用策略

回头看那个 800 行 switch——值得用策略吗?值得,因为促销规则确实在持续增加,且规则之间相互独立。但如果只是订单类型有 3 种、固定不变,老老实实 switch 反而读起来更快。

策略模式真正教会我的,不是"用对象替代 switch",而是把变化点显式化——让未来的维护者一眼看到"这里是会变的地方,加新东西在这里加"。这比模式本身重要得多。

如果你也在纠结某段代码要不要抽策略,不妨问自己三个问题:分支会持续增加吗?分支之间独立吗?分支数 ≥ 3 吗?三个都"是"再动手,否则保持简单。

Go 标准库里策略模式其实随处可见——sort.Interface 是策略(你提供 Less/Swap/Len 算法,sort 提供排序骨架)、http.HandlerFunc 是策略、io.Reader/Writer 也是策略。看懂这些设计,比记住模式定义有用得多。


想深入的同学,以下是 Go 标准库里看策略模式的最佳入口:

  • sort/sort.goSort 函数 —— 看它如何把排序算法骨架与 Interface 这个策略分离
  • net/http/server.goHandler 接口 —— 看一个方法撑起整个 HTTP 框架的策略设计
  • io/io.goReader/Writer —— Go 最纯粹的战略级策略接口

下一篇讲责任链,它和策略结构几乎一样,但意图完全不同——一个选算法,一个传请求。


关于十三Tech 资深服务端研发,AI 实践者,专注分享真实可落地的技术经验。 相信 AI 是程序员的最佳搭档。

联系方式:569893882@qq.com GitHub:@TriTechAI