大家好,我是十三!欢迎来到十三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。
下面这段是我项目里实际在用的写法。看 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」这种表面功夫,而是让算法变化点收敛到一处。
策略本身怎么写?没什么神秘的,就是实现 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.go的Sort函数 —— 看它如何把排序算法骨架与Interface这个策略分离net/http/server.go的Handler接口 —— 看一个方法撑起整个 HTTP 框架的策略设计io/io.go的Reader/Writer—— Go 最纯粹的战略级策略接口
策略模式不是 if-else 清洁剂
策略模式解决的是算法族替换,不是把所有分支都赶出代码。两个稳定分支写成 switch 很好读,硬拆成接口和三个文件反而会让人绕路。真正值得抽策略的,是算法数量会增长、需要独立测试、或者需要在运行时按配置切换的场景。
还有一个常被忽略的点:策略选择本身也要有归属。业务流程到处 NewXXXStrategy(),只是把 if-else 换了个地方扩散。比较稳的写法,是把选择集中在工厂或上下文里,让主流程只面对一个稳定接口。
三个容易被追问的点
评审追问一:什么时候 if-else 比策略更好?
分支少、稳定且只在一个地方出现时,if-else 更直接。策略适合算法数量增长、需要运行时替换或独立测试的场景。
评审追问二:策略选择应该放在哪里?
放在上下文或工厂里集中选择,业务流程只依赖策略接口。到处 new 策略会把复杂度扩散出去。
评审追问三:策略模式怎么防止抽象过早?
先让两个以上真实算法自然出现,再抽接口;不要为了“可能扩展”提前把简单逻辑拆成一堆类型。
下一篇讲责任链,它和策略结构几乎一样,但意图完全不同——一个选算法,一个传请求。
关于十三Tech 资深服务端研发,AI 实践者,专注分享真实可落地的技术经验。 相信 AI 是程序员的最佳搭档。
联系方式:569893882@qq.com GitHub:@TriTechAI
