应优先使用 factory 函数而非单例工厂函数(newxxx)创建需多次实例化、配置各异、生命周期独立的对象(如 http.client、sql.db),单例仅适用于真正全局唯一且状态共享的组件(如日志器、配置管理器);factory 支持选项模式、延迟初始化、错误显式返回与测试可替换性,而 sync.once 单例易因忽略错误、阻塞 io 或循环依赖引发隐患。

什么时候该用 Factory 函数而不是单例
工厂函数(NewXxx)适用于需要**多次创建、配置不同、生命周期独立**的对象,比如 *http.Client、*sql.DB、自定义的 Service 实例。单例只适合真正全局唯一、状态共享、且初始化后不随调用上下文变化的对象(如日志器、配置管理器、全局连接池)。
常见错误现象:global.DB 被直接导出为包级变量,结果测试时多个 case 共享同一连接池状态,事务或 mock 失败;或者误把本该按租户隔离的 UserCache 做成单例,导致数据串扰。
- 对象需支持不同参数组合(如超时、重试、endpoint)→ 必须用工厂
- 对象内部持有可变状态(如缓冲区、计数器、本地缓存)→ 不适合单例
- 单元测试中需注入 mock 实现 → 工厂 + 接口返回才能替换,单例无法覆盖
- 初始化依赖运行时输入(如从 flag 或 env 读取 config)→ 工厂可传参,单例只能靠包级 init 或全局变量间接传递
sync.Once 单例的典型陷阱
sync.Once 确保初始化只执行一次,但它**不保证初始化成功**——如果 once.Do 里 panic 或返回 error 被忽略,后续调用会得到 nil 或未完全初始化的实例。
使用场景:配置加载失败应提前暴露,而不是静默返回空指针导致下游 nil pointer dereference。
- 初始化逻辑必须显式处理 error,不能只用
_忽略(如zap.NewProduction()可能返回 error) - 不要在
once.Do里做阻塞 IO(如真实连接数据库),否则首次调用会卡住所有 goroutine - 避免在单例初始化中调用其他单例的
GetInstance(),容易引发死锁或初始化顺序依赖 - 包级变量名别用
instance这类泛名,而应体现用途(如configMgr、loggerInst),方便调试时识别
Factory 函数怎么支持可选配置又不爆炸
硬编码默认值或为每个选项加参数都会让函数签名失控。Go 社区通用解法是 option 模式,但实现细节容易出错。
参数差异:不是所有配置都该在构造时决定。比如连接池大小可在 NewDB() 时设,但实际建连(Open())应延迟到首次使用前,避免启动即失败。
- 定义不可导出的
type option func(*T),每个WithXXX函数只负责字段赋值,不做副作用 - 工厂函数接收
...option,按传入顺序应用,后项覆盖前项(如WithTimeout(10s), WithTimeout(5s)最终生效 5s) - 避免在 option 里调
os.Open、net.Dial等 IO 操作,它们不属于“配置”,而是初始化行为 - 若初始化可能失败(如解析 config 文件),工厂函数必须返回
(*T, error),不能只返回指针
接口返回 vs 具体类型返回
工厂函数返回具体类型(如 *MyClient)是 Go 的惯用做法;返回 interface{} 或泛型 any 是反模式,它放弃编译检查,IDE 补全失效,且调用方无法知道该对象能做什么。
性能影响:返回接口类型本身无开销,但若为满足接口而做类型转换(如 return interface{}(c)),会触发接口底层的动态分配和装箱。
- 优先返回具体指针类型(
*MyType),符合 Go 对象可变性的共识 - 若需解耦,应返回明确的接口(如
type Client interface { Do() error }),且该接口由调用方定义或导入,而非工厂包内定义 - 不要为了“看起来灵活”而返回
io.Reader这种宽泛接口,除非对象真只提供读能力 - 工厂函数名严格用
NewXxx,这是 Go toolchain(如 godoc、gopls)识别构造函数的约定
sync.Once 包一层全局实例。容易被忽略的是初始化时机与错误传播路径:工厂失败可由调用方处理,单例失败却可能沉默地污染整个进程。golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











