策略模式的核心是把会变化的算法封装成独立的可替换单元,让调用方依赖抽象而非具体实现。在Golang里没有类继承,我们直接用接口定义行为,各种算法各自实现该接口,然后在运行时把不同实现赋值给同一个接口变量,就完成了动态切换。这种方式能把促销规则、计费模型、压缩方式等易变逻辑从主流程里剥离出来。

一、定义策略接口与具体算法
第一步是抽象出算法的统一入口。假设我们在做一个计算工具,支持加、减、乘三种运算,它们都有一个接收两个int返回int的行为。我们用一个Calculator接口来表达这个行为,任何算法只要实现了Compute方法,就自动成为该策略的一员。
具体算法写成独立的结构体,彼此互不影响。下面的代码里Add、Sub、Mul分别实现接口,未来要加除法或幂运算,只需新增结构体,不用碰原有代码。这也符合开闭原则:对扩展开放,对修改关闭。
package main
import "fmt"
// Strategy接口定义
type Calculator interface {
Compute(a, b int) int
}
// 加法策略
type Add struct{}
func (Add) Compute(a, b int) int {
return a + b
}
// 减法策略
type Sub struct{}
func (Sub) Compute(a, b int) int {
return a - b
}
// 乘法策略
type Mul struct{}
func (Mul) Compute(a, b int) int {
return a * b
}
func main() {
var c Calculator
c = Add{}
fmt.Println(c.Compute(4, 2)) // 6
c = Sub{}
fmt.Println(c.Compute(4, 2)) // 2
}
二、通过map实现动态切换
实际项目中,算法往往由外部参数决定,例如接口传来的type字段或配置文件里的模式名。我们可以把策略名和对应结构体放进一个map,在请求进来时查表拿到具体实现,再执行计算。这样主流程完全不关心算法细节。
下面示例用一个init函数初始化注册表,handler根据name从map取策略。若需要热加载新算法,只要向map写入新实现即可,甚至可以通过插件机制在运行时注册。注意map要配合读写锁,避免并发注册和调用产生竞态。
package main
import (
"fmt"
"sync"
)
type Calculator interface {
Compute(a, b int) int
}
type Add struct{}
func (Add) Compute(a, b int) int { return a + b }
type Sub struct{}
func (Sub) Compute(a, b int) int { return a - b }
var (
registry = make(map[string]Calculator)
lock sync.RWMutex
)
func register(name string, c Calculator) {
lock.Lock()
registry[name] = c
lock.Unlock()
}
func get(name string) (Calculator, bool) {
lock.RLock()
c, ok := registry[name]
lock.RUnlock()
return c, ok
}
func init() {
register("add", Add{})
register("sub", Sub{})
}
func handler(name string, a, b int) int {
c, ok := get(name)
if !ok {
panic("strategy not found")
}
return c.Compute(a, b)
}
func main() {
fmt.Println(handler("add", 3, 5))
fmt.Println(handler("sub", 3, 5))
}
三、用函数类型简化轻量策略
当算法逻辑很短、不需要维护状态时,没必要定义一堆结构体。Golang的函数是一等公民,我们可以直接用函数类型当策略。这样代码更紧凑,也更容易用闭包携带配置参数,比如税率、折扣率等。
下面把策略接口换成type Op func(int, int) int,注册表里存的是函数。在创建策略时,通过闭包把固定参数包进去,返回的是一个已经绑定了配置的算法函数。这种方式在规则简单、无状态的场景下非常合适,缺点是不方便挂载复杂方法。
package main
import "fmt"
// 用函数类型作为策略
type Op func(int, int) int
var funcs = map[string]Op{
"add": func(a, b int) int { return a + b },
"mul": func(a, b int) int { return a * b },
}
// 闭包生成带系数的策略
func scale(factor int) Op {
return func(a, b int) int {
return (a + b) * factor
}
}
func main() {
fmt.Println(funcs["add"](2, 3))
s := scale(10)
fmt.Println(s(2, 3)) // (2+3)*10 = 50
}
四、在业务里做支付路由切换
策略模式常见于支付网关选择。不同渠道(微信、支付宝、银联)的下单与退款逻辑差异很大,但对外暴露的接口一致。我们可以定义PayStrategy,每个渠道实现Pay和Refund,订单服务持有一个当前策略,根据商户配置在初始化或每次请求时切换。
下面的代码展示订单服务如何通过setStrategy动态更换渠道,而不改变调用方的代码。如果以后接入云闪付,只需新增Yinlian结构体并注册,原有下单流程一行都不用改。这种结构也让单元测试更容易:测试时塞一个假策略即可。
package main
import "fmt"
type PayStrategy interface {
Pay(orderID string, amount int) string
}
type Wechat struct{}
func (Wechat) Pay(orderID string, amount int) string {
return fmt.Sprintf("wechat pay %s:%d", orderID, amount)
}
type Alipay struct{}
func (Alipay) Pay(orderID string, amount int) string {
return fmt.Sprintf("alipay pay %s:%d", orderID, amount)
}
type OrderService struct {
strategy PayStrategy
}
func (o *OrderService) setStrategy(s PayStrategy) {
o.strategy = s
}
func (o *OrderService) Checkout(id string, amt int) string {
return o.strategy.Pay(id, amt)
}
func main() {
svc := &OrderService{}
svc.setStrategy(Wechat{})
fmt.Println(svc.Checkout("A100", 200))
svc.setStrategy(Alipay{})
fmt.Println(svc.Checkout("A100", 200))
}
五、方案对比与注意点
结构体实现接口适合有状态、方法多的算法,易于扩展和单测;函数类型适合无状态短逻辑,写法轻巧但表达能力有限。map注册方便按名切换,但要处理并发安全;直接赋值适合启动时决定策略的简单程序。
使用策略模式时要避免策略膨胀:如果算法之间仅有参数不同,应优先考虑用配置加统一函数,而不是每种组合都建一个结构体。另外,调用方不应感知具体类型,切换逻辑建议收敛到工厂或上下文对象里,保持高层代码干净。
| 方式 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 结构体+接口 | 复杂多变算法 | 易扩展、可单测 | 代码量偏多 |
| 函数类型 | 无状态短逻辑 | 写法简洁 | 难挂复杂状态 |
| map注册 | 按名动态路由 | 切换灵活 | 需并发控制 |
策略模式不是消除变化,而是把变化关进独立的笼子里,让主流程保持稳定。