策略模式属于行为型设计模式,目的是将一组可互换的算法封装起来,让使用算法的代码不依赖具体实现。在Golang里,我们通常用接口来定义算法契约,再用不同的结构体实现该接口,最后在程序运行期间根据外部条件选择并切换具体的结构体实例。这种方式比在业务函数里写一大堆if-else或switch直观得多,也更容易扩展和维护。

一、定义策略接口与具体算法
第一步是抽象出所有算法都要遵守的方法签名。假设我们在做一个计算折扣的服务,折扣算法以后可能会不断增加,那么可以先定义一个DiscountStrategy接口,只包含一个计算折扣后价格的方法。
接着编写两个具体的策略:普通会员折扣和超级会员折扣。它们都实现同一个接口,因此在使用的地方可以被互相替换。下面给出最基础的接口与实现代码:
package main
import "fmt"
// DiscountStrategy 折扣策略接口
type DiscountStrategy interface {
Calc(price float64) float64
}
// NormalMember 普通会员打九五折
type NormalMember struct{}
func (n NormalMember) Calc(price float64) float64 {
return price * 0.95
}
// SuperMember 超级会员满100减20,否则打八折
type SuperMember struct{}
func (s SuperMember) Calc(price float64) float64 {
if price >= 100 {
return price - 20
}
return price * 0.8
}
func main() {
var strategy DiscountStrategy
strategy = NormalMember{}
fmt.Println(strategy.Calc(120)) // 114
strategy = SuperMember{}
fmt.Println(strategy.Calc(120)) // 100
}
上面的代码展示了策略模式最基本的形态:变量strategy的类型是接口,但运行时指向了不同的结构体。只要新算法也实现了Calc方法,就能直接赋值给这个变量,业务代码完全不用改。
这种写法的好处是单一职责清晰,每个算法自己管自己的逻辑。如果以后要加一个企业客户折扣,只需新增一个结构体,不会触动原有函数。不过此时切换还是硬编码在main里,接下来我们看怎么做到真正的动态切换。
二、通过映射表实现动态切换
动态切换的核心,是把策略名称和对应的实例建立一个注册关系,程序根据外部传入的字符串或配置去查表拿到具体策略。最常见的方式是用一个map[string]DiscountStrategy来做策略仓库。
我们写一个注册函数,在程序初始化时把支持的算法放进去。处理请求时,从用户参数里拿到会员类型,直接从map取策略并计算。示例代码如下:
package main
import (
"errors"
"fmt"
)
type DiscountStrategy interface {
Calc(price float64) float64
}
type NormalMember struct{}
func (n NormalMember) Calc(price float64) float64 {
return price * 0.95
}
type SuperMember struct{}
func (s SuperMember) Calc(price float64) float64 {
if price >= 100 {
return price - 20
}
return price * 0.8
}
// 策略仓库
var strategies = make(map[string]DiscountStrategy)
// Register 注册策略
func Register(name string, s DiscountStrategy) {
strategies[name] = s
}
// GetStrategy 获取策略
func GetStrategy(name string) (DiscountStrategy, error) {
s, ok := strategies[name]
if !ok {
return nil, errors.New("策略不存在")
}
return s, nil
}
func init() {
Register("normal", NormalMember{})
Register("super", SuperMember{})
}
func main() {
userType := "super"
price := 150.0
s, err := GetStrategy(userType)
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("原价%.2f, 实付%.2fn", price, s.Calc(price))
}
这样一来,切换算法完全由userType这个变量控制。如果在Web接口里,这个值来自URL参数或JSON请求体,就做到了根据外部输入动态选择算法。
使用map注册的方式简单明了,但要注意并发安全。如果允许运行时动态注册新策略,多个goroutine同时读写map会panic。可以用sync.RWMutex把读写包起来,读多写少时性能影响很小。
三、结合配置与函数式策略
除了结构体实现接口,Golang里也可以直接用函数类型当策略,这样代码更轻量。我们定义type DiscountFunc func(float64) float64,然后map里存函数而不是结构体。
再进一步,可以把策略名和算法对应关系放到配置文件或数据库里,程序启动时加载。这样运营想新增一个节日折扣,改配置重启即可,不用动代码。下面是用函数式策略的示例:
package main
import "fmt"
// DiscountFunc 折扣函数类型
type DiscountFunc func(price float64) float64
var strategyFuncs = map[string]DiscountFunc{
"normal": func(p float64) float64 { return p * 0.95 },
"super": func(p float64) float64 {
if p >= 100 {
return p - 20
}
return p * 0.8
},
}
func applyStrategy(name string, price float64) (float64, bool) {
f, ok := strategyFuncs[name]
if !ok {
return 0, false
}
return f(price), true
}
func main() {
if final, ok := applyStrategy("normal", 200); ok {
fmt.Println(final) // 190
}
}
函数式写法去掉了结构体定义,适合逻辑很短的算法。如果算法复杂、需要携带状态,还是用结构体实现接口更合适,因为结构体可以有字段保存中间数据。
无论用哪种形式,动态切换的本质都是“行为参数化”:把算法当数据一样传递和选择。在微服务或插件化系统里,还可以把策略实现编译成独立包,用反射或插件机制加载,不过那已经超出基础策略模式的范畴。
四、在HTTP服务中实战切换
把前面的思路放到一个HTTP接口里,就能看到策略模式在日常后端开发中的真实价值。我们写一个简易的net/http服务,根据查询参数type决定折扣策略,并返回计算结果。
这样前端或调用方只需改参数就能拿到不同算法的输出,后端新增算法也只是多注册一项。完整示例如下:
package main
import (
"encoding/json"
"net/http"
)
type DiscountStrategy interface {
Calc(price float64) float64
}
type NormalMember struct{}
func (n NormalMember) Calc(p float64) float64 { return p * 0.95 }
type SuperMember struct{}
func (s SuperMember) Calc(p float64) float64 {
if p >= 100 {
return p - 20
}
return p * 0.8
}
var strategies = map[string]DiscountStrategy{
"normal": NormalMember{},
"super": SuperMember{},
}
func discountHandler(w http.ResponseWriter, r *http.Request) {
t := r.URL.Query().Get("type")
price := 120.0
s, ok := strategies[t]
if !ok {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte("unknown type"))
return
}
result := map[string]float64{"final": s.Calc(price)}
json.NewEncoder(w).Encode(result)
}
func main() {
http.HandleFunc("/discount", discountHandler)
http.ListenAndServe(":8080", nil)
}
运行后访问 /discount?type=super 就能得到超级会员价格。这个结构让算法和接口处理彻底解耦: handler只关心取策略和调Calc,不关心里面怎么算。
如果以后接了新业务,比如地区定价策略,只要再定义一套接口和map,或者把现有map的value换成组合策略,就能平滑扩展。这就是策略模式动态切换在Golang里最实用的落地方式。