go工厂模式核心是用map[string]func() interface注册制替代if/else,工厂函数须轻量、无副作用、不初始化依赖,支持延迟构造与测试替换。

Go 里工厂不是“要不要写”,而是“怎么写才不崩”。直接用 if/else 判断字符串返回结构体指针,上线三天就有人提 PR 改第四种类型——你得改工厂、测所有分支、祈祷没漏掉 nil 情况。真正的轻量可维护写法,就一条:用 map[string]func() Interface 配注册制。
为什么不能把工厂写成一堆 if/else
看似省事,实则埋雷:
-
if typ == "json"这类分支每加一个,就要动主逻辑,违反开闭原则 - 测试时无法替换某类实现(比如想 mock
YAMLProcessor,但它被硬塞在 if 分支里) - 无法传参构造:
&FileLogger{filename: cfg.LogFile}写死在分支里?配置一变就得改代码 - 编译器不检查 map key 覆盖,运行时调用
NewProcessor("avro")直接 panic
注册表必须用 func() Interface,不是 Interface
关键区别在这里:processors["json"] = func() Processor { return &JSONProcessor{} } 和 processors["json"] = &JSONProcessor{} 完全不同。
- 前者是函数值,调用时才创建实例——支持依赖注入、参数化、延迟初始化
- 后者是立即实例化,启动就 new 出来,哪怕没人用也占内存、连 DB、开文件
- 若构造需读配置或连 DB,
func()可在调用时捕获当前上下文;而预实例化会卡死在 init 阶段 - 单元测试中可轻松覆盖:
processors["test"] = func() Processor { return &MockProcessor{} }
注册时机:init() 简单,但 main() 显式更可控
多数教程教你在 init() 里注册,但它有硬伤:
- 依赖外部配置(如从
Viper读log.type)时,init()执行早于配置加载,拿不到值 - 多个包的
init()执行顺序不确定,若 A 包工厂依赖 B 包的某个Register,可能 panic - 热加载场景下,
init()只跑一次,没法动态增删类型
更稳妥的做法是在 main() 开头集中注册:
func main() {
Register("json", func() Processor { return &JSONProcessor{} })
Register("yaml", func() Processor { return &YAMLProcessor{Decoder: yaml.NewDecoder(os.Stdin)} })
// 其他注册...
http.ListenAndServe(":8080", nil)
}
抽象工厂只在需要产品族协同时才用
别一上来就搞 AbstractFactory 接口 + HotelFactory + FlightFactory。90% 的场景,单个 map[string]func() Service 就够了。
- 只有当你必须保证一组对象强绑定(比如
Logger+Cache+DB必须同属dev或prod实现),且切换时要原子生效,才值得上抽象工厂 - 抽象工厂本身不解决“怎么创建”,它解决的是“哪一套一起创建”——本质是多层注册表嵌套,复杂度陡增
- Go 里抽象工厂常退化为一个函数:
func NewEnvFactory(env string) (Logger, Cache, DB),比接口+结构体组合更直白
真正容易被忽略的点是:工厂函数必须无副作用、不初始化依赖。它只负责 new 出来,不该打开文件、连数据库、启动 goroutine——那是具体类型自己的 NewXXX() 该干的事。否则工厂就成了隐藏的初始化入口,谁调用谁背锅。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











