sync.once 是并发安全的延迟初始化原语,非单例模式本身;它通过原子操作+互斥锁确保初始化逻辑仅执行一次,且所有调用者安全获取结果,失败不重试、panic后永久失效。

sync.Once 不是单例模式的实现,而是延迟初始化的并发安全原语;单例只是它最常见的一种用法。 它解决的核心问题只有一个:在多 goroutine 竞发下,确保某段初始化逻辑只执行一次,且所有调用者都能安全拿到结果。别把它和“单例设计模式”混为一谈——模式是思想,sync.Once 是工具。
sync.Once.Do 为什么能保证“只执行一次”
它靠的是一个 uint32 标志位 + 原子操作 + 互斥锁的组合:atomic.CompareAndSwapUint32(&o.done, 0, 1) 先抢“执行权”,失败者直接返回;成功者加锁进入慢路径,真正执行函数,并在最后把 done 设为 1(不依赖原子写,因为此时已上锁)。这不是“谁快谁先”,而是“所有人等唯一一次执行完成”。
- 第一次调用
Do时,atomic.LoadUint32(&o.done)返回 0,触发doSlow - 后续所有调用都立即返回,不阻塞、不重试、不报错
- 一旦
Do里的函数 panic,done仍会被设为 1,该Once实例永久失效 —— 这是最容易被忽略的坑
用 sync.Once 实现带 error 的懒加载单例
Do 方法签名是 func(f func()),不接受参数也不返回值,所以“初始化”和“取值”必须拆开。典型做法是用包级变量承载结果与错误:
var (
instance *DBClient
once sync.Once
err error
)
func GetDBClient() (*DBClient, error) {
once.Do(func() {
instance, err = newDBClient() // 可能返回 (*DBClient, error)
})
return instance, err
}
-
err必须是包级变量,否则闭包内赋值对外不可见 - 如果
newDBClient()panic,err保持零值,调用方拿到nil, nil,需额外判空或捕获 panic(但不建议在Do里 recover) - 不要指望
sync.Once帮你重试 —— 它没有重试语义,失败就是失败
为什么不能用 if + Mutex 替代 sync.Once
手写 double-checked locking 看似可行,但极易出错:
-
if instance == nil判断本身不是原子的,多个 goroutine 可能同时通过 - 即使加锁,初始化语句如
instance = &MyService{}可能被编译器/CPU 重排,导致其他 goroutine 看到未完全初始化的对象(如字段为零值) -
sync.Once内部的内存屏障和锁顺序已严格保证初始化完成后再更新done,无需你操心指令重排 - 每次读取都走原子判断,比每次都要 lock/unlock 更轻量
真正复杂的地方在于:你需要自己处理初始化失败的传播路径,以及确认初始化逻辑本身是幂等的(比如注册 handler、启动 goroutine)。sync.Once 只管“执行一次”,不管“执行得对不对”。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











