go语言通过接口+组合实现策略模式,强调算法可插拔、可测试、可配置;strategy接口需明确定义输入输出边界与语义化方法名;concretestrategy须隔离副作用;context需支持运行时setstrategy并校验非空。

Go 语言本身不提供运行时多态或继承,但用接口 + 组合就能干净地实现策略模式——关键不是“模拟 OOP”,而是让算法可插拔、可测试、可配置。
定义 Strategy 接口必须满足行为契约
接口不是为了“看起来像设计模式”,而是为了约束所有算法的输入输出边界。比如语言学习场景中常见的「单词记忆强度评估」:
- 输入统一是
[]string(待复习词列表)和当前时间戳time.Time - 输出统一是
[]string(推荐复习子集)或错误error - 接口方法名要语义明确,比如
Recommend,别叫Do或Handle
错误示例:type ReviewStrategy interface { Calc([]string) []string } —— 缺少上下文参数、没声明 error、方法名模糊,后续加权重或日志就不得不改接口,破坏所有实现。
ConcreteStrategy 实现要隔离副作用
每种算法(如 SM-2、FSRS、简单按天轮询)应只做计算,不读配置、不查 DB、不打日志。这些由上下文或外部注入:
-
SM2Strategy只根据卡片历史和间隔公式算下次复习时间 -
FSRSConfig字段可以带decay float64等参数,但不包含*sql.DB或*log.Logger - 如果某个策略需要用户偏好,通过构造函数传入,而不是在
Recommend方法里调用GetUserConfig()
否则会导致单元测试难写、策略复用受限、切换时隐式依赖爆炸。
Context 要支持运行时 SetStrategy 且校验非空
上下文结构体(比如 ReviewScheduler)持有接口字段,但必须在调用前检查是否为 nil:
func (s *ReviewScheduler) Recommend(words []string, now time.Time) ([]string, error) {
if s.strategy == nil {
return nil, fmt.Errorf("strategy not set: call SetStrategy first")
}
return s.strategy.Recommend(words, now)
}
常见坑:
- 直接在
NewReviewScheduler()里硬编码默认策略,导致初始化后无法切换 - 用指针接收者实现
SetStrategy却传值,实际没生效(s.strategy = strategy修改的是副本) - 把策略存在全局变量或单例里,测试时互相污染
切换策略的实际触发点通常是配置或用户动作
真实语言学习 App 中,策略切换往往来自:
- 用户在设置页选择「使用 FSRS 算法」→ 触发
scheduler.SetStrategy(&FSRS{}) - 配置文件加载后动态替换:
if cfg.Algorithm == "sm2" { scheduler.SetStrategy(&SM2Strategy{}) } - A/B 测试分流:
if rand.Float64()
注意:不要在每次 Recommend 调用前都重新 Set —— 这会掩盖状态管理问题,也失去策略复用的意义。
最易被忽略的点:策略实例本身可能有内部状态(比如 FSRS 的 w 参数数组),如果多个上下文共享同一个策略实例,状态会被交叉污染。要么确保策略是无状态的,要么每次 SetStrategy 都传新实例。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











