饿汉式单例启动即初始化、线程安全但可能浪费内存;懒汉式首次调用才创建,需 sync.once 保证并发安全;go 1.21+ 可用 sync.oncevalue 简化返回值处理;选型取决于初始化成本与使用确定性。

饿汉式单例:启动时就初始化,但可能浪费内存
饿汉式在包加载时就创建实例,init() 函数或全局变量赋值就能搞定。它天然线程安全,因为 Go 的包初始化是串行且只执行一次的。
常见错误是误以为“只要用了 sync.Once 就是懒汉式”,其实没调用 GetInstance() 就已分配对象,哪怕整个程序压根没用到这个单例。
- 适合:配置加载、日志器等启动必用、构造开销小的组件
- 不适合:数据库连接池、大缓存对象等可能闲置、初始化耗时或依赖运行时参数的场景
- 内存影响:实例对象及其依赖(如
*sql.DB)会在程序生命周期全程驻留,GC 无法回收
懒汉式单例:首次调用才创建,但必须处理并发竞争
懒汉式靠 sync.Once 保证只初始化一次,典型结构是封装一个私有变量 + GetInstance() 函数。它延迟了资源分配,但多协程首次并发调用时,sync.Once 是唯一靠谱的同步机制。
容易踩的坑是手写双重检查锁(Double-Check Locking),Go 中不推荐——sync.Once 更简洁、无序执行风险,且性能差异可忽略。
立即学习“go语言免费学习笔记(深入)”;
- 必须用
sync.Once,不要自己用sync.Mutex加锁判断 - 初始化函数里别做阻塞操作(比如等待网络响应),否则所有后续调用都会卡住
- 如果初始化失败(如 DB 连接超时),建议 panic 或返回 error 并缓存失败状态,避免反复重试
Go 1.21+ 的替代方案:sync.OnceValue 更轻量
Go 1.21 引入了 sync.OnceValue,它和 sync.Once 行为一致,但能直接返回初始化结果,省去额外变量声明。对返回值为指针或接口的单例特别友好。
注意:它不能处理初始化失败后重试,一旦 func() 返回非零值,后续调用永远返回该值;若需错误恢复能力,仍得自己封装逻辑。
- 适用场景:纯内存对象、无副作用的工厂函数(如
json.Encoder实例) - 不适用:需要区分“未初始化”“初始化中”“初始化失败”三种状态的复杂组件
- 兼容性:低于 Go 1.21 的项目无法使用,别盲目升级代码
真实项目里怎么选:看初始化成本和使用确定性
没有银弹。饿汉式不是“过时”,懒汉式也不是“更高级”。关键看两点:这个单例是不是启动即用?它的初始化会不会触发外部依赖或耗时操作?
比如微服务中读取 config.yaml 的配置管理器,通常饿汉式更稳——早暴露问题,避免运行中 panic;而 Redis 客户端连接,懒汉式更合理,毕竟测试环境可能根本不用它。
- 如果初始化函数会调用
http.Get、sql.Open、os.Open,优先懒汉式 - 如果单例只是个空结构体或只做简单字段赋值,饿汉式更干净
- 混用也常见:用饿汉式加载配置,再基于配置在懒汉式里构建 DB/Redis 实例
最常被忽略的是初始化顺序依赖——饿汉式实例 A 依赖懒汉式实例 B,但 A 的 init() 执行时 B 还没创建,这时 panic 不会告诉你原因,只会报 nil pointer dereference。










