大家好,我是十三!欢迎来到十三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 最纯粹的战略级策略接口
下一篇讲责任链,它和策略结构几乎一样,但意图完全不同——一个选算法,一个传请求。
关于十三Tech 资深服务端研发,AI 实践者,专注分享真实可落地的技术经验。 相信 AI 是程序员的最佳搭档。
联系方式:569893882@qq.com GitHub:@TriTechAI
