Go中直接用map[string]func() Strategy不够用,因其仅支持无参构造,无法传参、处理依赖或暴露错误;且不支持运行时注册、无类型校验、易引发panic。

为什么直接用 map[string]func() Strategy 不够用
因为策略初始化常需传参、依赖外部资源,或涉及耗时操作(如读配置、连数据库),而 map[string]func() Strategy 只能返回无参构造的实例,无法传递上下文或错误。更关键的是,它不支持运行时注册新策略,也不校验实现是否满足接口契约。
实操建议:
- 用
map[string]func(...interface{}) (Strategy, error)替代纯函数映射,允许传参并暴露初始化失败可能 - 工厂内部加锁(
sync.RWMutex)保护注册表,避免并发注册冲突 - 注册时做一次类型断言验证:
_, ok := strategyConstructor(...) .(Strategy),防止后期调用 panic - 避免在构造函数里做重操作——工厂只负责“创建”,不负责“启动”或“连接”
如何让 Strategy 接口既灵活又可测试
接口定义太宽泛(比如只含 Execute())会导致策略行为不可控;定义太具体(比如带 WithContext(context.Context)、WithTimeout(time.Duration))又让每个实现都得处理冗余逻辑。折中做法是让接口只声明核心能力,把通用参数通过工厂注入或策略初始化时传入。
实操建议:
- 接口保持极简:
type Strategy interface { Do(input interface{}) (interface{}, error) } - 若需上下文控制,不塞进接口,而是让工厂返回一个闭包包装器:
func(ctx context.Context) Strategy - 测试时,用匿名结构体实现接口,不依赖真实依赖;例如:
mock := struct{ Strategy }{},再覆盖Do方法 - 避免在接口中暴露 setter 方法(如
SetLogger(*log.Logger)),这会让策略状态难追踪,改用构造时注入
注册策略时怎么避免 init 顺序陷阱
Go 的 init() 函数执行顺序不确定,如果多个包各自调用 Register("foo", newFooStrategy),可能因导入顺序不同导致某些策略未被注册。更隐蔽的问题是:测试中单独跑某个策略文件,init 没触发,工厂查不到。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
实操建议:
- 禁用全局
init()注册,改为显式调用factory.Register("name", constructor) - 在
main()或应用启动入口统一注册,或封装为RegisterAll()函数集中管理 - 注册失败必须返回 error 并显式检查,不能静默忽略;常见失败包括重复 key、构造函数返回 nil
- 测试中用
t.Cleanup()清空注册表,避免测试间污染:defer func() { factory.Clear() }()
如何安全地从字符串名解析并创建策略
factory.Get("payment_alipay") 这类调用看似简单,但实际容易踩坑:名称拼写错误返回 nil 不报错、参数类型不匹配 panic、构造失败却没透出 error。用户真正需要的不是“拿到实例”,而是“拿到可用实例或明确失败原因”。
实操建议:
- 不要返回裸指针或接口值,强制返回
(Strategy, error)元组 - 错误信息必须包含策略名和失败原因,例如:
"failed to create strategy 'sms_twilio': missing TWILIO_AUTH_TOKEN env var" - 避免用
panic处理业务级错误(如配置缺失),仅用于开发期断言(如接口未实现) - 若策略需传参,推荐用结构体选项模式:
factory.Get("notify_email", WithSMTPAddr("smtp.gmail.com:587")),比不定长...interface{}更易维护
最易被忽略的一点:策略工厂本身不该持有任何业务状态。它的职责只是按名分发构造函数——所有策略间的隔离性、生命周期、依赖注入,都应该在构造函数内部完成,而不是靠工厂去“管理”策略实例。










