sync.Once是Go实现线程安全单例的唯一推荐方式,因其底层用原子操作+互斥锁组合确保初始化仅执行一次、所有goroutine等待完成,避免手动加锁漏判空、双重检查失效或半初始化对象等问题。

sync.Once 是 Go 语言实现线程安全单例的唯一推荐方式,其他手动加锁或双重检查(double-check)写法在 Go 中既不必要也不安全。
为什么不用自己写锁?
Go 的 sync.Once 底层用原子操作 + 互斥锁组合,已针对初始化场景做极致优化;自己用 sync.Mutex 或 sync.RWMutex 实现,容易漏掉竞态点,比如:未保护读、忘记 defer 解锁、或在 init() 里提前暴露未完成初始化的指针。
sync.Once 必须配合包级变量使用
单例对象本身必须是包级变量(非局部变量),否则每次调用函数都会新建一个 once 实例,失去“只执行一次”的意义:
- ✅ 正确:
var instance *Singleton在函数外定义,once也是包级变量 - ❌ 错误:
func GetInstance() *Singleton { var once sync.Once; ... }—— 每次调用都新建once,完全失效
初始化逻辑不能依赖外部可变状态
once.Do() 内部函数一旦执行,就不会再重试。如果初始化失败(比如配置加载出错、网络不可达),后续所有 GetInstance() 都会返回 nil 或 panic 状态,无法自动恢复:
- 不要在
once.Do里做带副作用且可能失败的 IO 操作(如连接数据库) - 若必须处理失败,应在
once.Do外层封装重试逻辑,或改用 lazy-init + error 返回(即func GetInstance() (*Singleton, error)) - 常见错误:在
once.Do里调用log.Fatal或os.Exit,导致整个进程退出,而非仅初始化失败
别把单例当全局配置容器用
单例对象本身应只负责“唯一性”和“延迟初始化”,而不是塞进一堆配置字段、方法、甚至 HTTP 客户端、DB 连接池等复杂依赖:
立即学习“go语言免费学习笔记(深入)”;
- ❌ 反模式:
type Singleton struct { DB *sql.DB; Config map[string]string; Logger *zap.Logger; ... } - ✅ 推荐:单例只持核心状态(如
data string),其他依赖通过构造函数参数注入,或由上层统一管理生命周期 - 真正需要“全局可访问”的不是单例结构体,而是它所代表的契约——比如
ConfigProvider接口,由单例实现并导出


















