动态策略加载依赖接口抽象、运行时注册与反射配置绑定,禁止硬编码或构建标签;策略须实现统一接口并集中注册,配置通过config标签反射注入,每次使用前动态创建实例并加载配置。

动态策略加载不是靠“自动发现包”实现的,而是靠接口抽象 + 运行时注册 + 反射驱动配置绑定。硬编码 map[string]Strategy 或依赖构建标签(//go:build)都扛不住线上热插拔需求。
策略必须用 interface 定义,且注册入口要统一
每个策略实现必须满足同一接口,比如:
type Strategy interface {
Execute(data map[string]interface{}) (interface{}, error)
Name() string
}
注册不能散落在各处,得集中到一个可导出函数里,比如 RegisterStrategy(name string, fn func() Strategy)。所有策略包在自己的 init() 函数中调用它,确保服务启动时全部就位。常见错误是漏掉 import _ "xxx/strategy/email" —— Go 不会自动执行未被引用的包的 init()。
- 策略名必须全局唯一,重复注册应 panic 或 warn 并跳过(避免静默覆盖)
- 注册函数内部用
sync.Once保护首次初始化,但注册本身不限次数(允许测试重载) - 不要把策略实例缓存成全局变量,而应每次调用
fn()创建新实例 —— 避免状态污染
配置映射靠反射 + tag,不依赖固定字段名
策略配置结构体字段需带 config: 标签,例如:
type EmailStrategyConfig struct {
SMTPHost string `config:"smtp_host"`
Timeout int `config:"timeout_ms"`
Retry bool `config:"retry_enabled"`
}
加载时传入策略实例指针和配置 map,用 reflect.ValueOf(cfg).Elem() 获取可修改值,再遍历字段匹配 config tag。关键点:
- 必须传指针,否则
reflect.Value.Set()会 panic - 字段必须首字母大写(导出),否则反射无法写入
- 嵌套结构体也要递归处理,比如
Database DatabaseConfig `config:"database"` - 类型不匹配时(如配置给的是字符串,字段是 int),应明确报错,而不是静默跳过
运行时按名获取策略并注入配置,不是启动时绑定
业务代码拿到策略名(比如从数据库查出 "email_v2")后,才去注册表里找对应构造函数,再 new 实例、再绑定配置:
fn, ok := strategyRegistry["email_v2"]
if !ok {
return errors.New("unknown strategy")
}
s := fn()
if err := LoadConfig(s, cfgMap); err != nil {
return err
}
s.Execute(data)
这个流程必须在每次使用前走完,不能把配置“一次性塞进全局策略变量”。原因:
- 不同请求可能需要不同参数组合(比如 A 用户用 SMTP,B 用户用 SendGrid)
- 配置可能动态变更(Consul 更新后需重新
LoadConfig,而非复用旧实例) - 避免多个 goroutine 共享同一策略实例导致竞态(尤其含字段缓存或连接池时)
viper.Unmarshal 不会自动更新已存在结构体字段
很多人用 viper.Unmarshal(&cfg) 初始化一次后就以为万事大吉,但 watch 配置变更后,viper.Unmarshal() 不会 diff 字段、也不会 patch 已有 struct —— 它只是重新解析当前缓存的配置数据,再新建一个结构体赋值。所以必须在每次配置变更回调里显式调用:
err := viper.Unmarshal(strategyConfig)
if err != nil {
log.Error("failed to unmarshal strategy config", "err", err)
return
}
// 然后拿 strategyConfig 去 LoadConfig 到新策略实例
更安全的做法是:每次变更都新建策略实例,而不是试图复用旧实例 + 覆盖字段。字段覆盖容易遗漏切片、map 或嵌套指针的 deep copy 逻辑。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











