Go工厂函数核心是收口new操作、返回接口实现解耦、通过注册构造函数支持运行时切换;必须返回接口而非具体类型,用map[string]func() Interface注册延迟实例化函数,避免硬编码与提前初始化。

Go 里工厂函数不是为了“模拟 OOP”,而是把 new 操作收口、避免调用方感知具体类型、支持运行时切换实现——核心就三点:返回接口、注册构造函数、延迟实例化。
工厂函数必须返回 interface,不能返回 *Struct
返回具体结构体指针(比如 *FileLogger)等于把实现细节暴露给调用方,后续加 Redis 日志或 mock 日志时,所有调用点都得改。而返回 Logger 接口,只要新类型实现了该接口,替换就完全透明。
- 接口定义要聚焦行为,例如:
type Logger interface { Log(string) },不带字段、不带私有方法 - 工厂签名应为
func NewLogger(cfg Config) Logger,不是func NewFileLogger() *FileLogger - 如果某处硬编码了
&FileLogger{},那它已经绕过了工厂,解耦失效
用 map[string]func() Interface 注册构造函数,别写 if/else 链
硬编码 switch 分支的工厂函数在新增类型时要改工厂、重新编译;用注册表方式,新增实现只需调用 RegisterLogger("kafka", func() Logger { return &KafkaLogger{} }),主逻辑零修改。
- 注册表声明为:
var loggerFactories = make(map[string]func() Logger),值是函数,不是实例 - 注册必须在
init()或启动期完成,不能在 handler 里边用边注册(并发不安全) - 读取注册表时加
sync.RWMutex(若存在运行时动态注册),否则只读场景可不用锁 - 别用
map[string]interface{}存实例——会提前触发初始化,且无法传参
配置参数别堆在函数签名里,优先用结构体或 Option 函数
当工厂需要 host、timeout、retries 等多个参数时,func NewDB(host, port, user, pass string, timeout, retries int) DB 很难维护,也违背单一职责。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
立即学习“go语言免费学习笔记(深入)”;
- 推荐结构体传参:
type DBConfig struct { Host, Port, User, Pass string; Timeout, Retries int },字段导出便于外部设置 - 更灵活的 Option 模式:
func NewDB(opts ...DBOption) DB,每个DBOption是闭包,例如WithTimeout(5 * time.Second) - heavy 初始化(如建连接、加载证书)不要放在工厂分发逻辑里,应下沉到具体类型的
NewXXX()中 - 配置校验(如
host非空)也应在具体类型构造时做,不是工厂的责任
别和单例混用,除非明确需要全局唯一实例
工厂函数默认每次调用都返回新实例;如果误在工厂里用了 sync.Once 或全局变量缓存实例,会导致并发下状态污染或内存泄漏。
- 日志、HTTP 客户端、数据库连接池等资源类对象,应由使用者自行决定是否复用,工厂只负责“创建”
- 真需要单例(如配置管理器),单独实现
GetInstance(),不要塞进工厂函数里 - 测试时若发现两个
NewLogger("file")返回的是同一个对象,说明工厂内部做了意外缓存,需排查
最易被忽略的一点:工厂本身不是“重逻辑容器”,它应该轻、快、无副作用。所有复杂初始化、依赖注入、连接建立,都应该交给具体类型自己的构造函数去完成——工厂只做路由和封装。

















