sync.Once.Do能保证只执行一次,因其用atomic.LoadUint32快速判断done状态,仅当done==0时才加锁二次确认,避免锁竞争;函数无论正常返回或panic,done均置1,后续调用直接返回。

sync.Once.Do 为什么能保证只执行一次
sync.Once.Do 内部用一个 done 字段(uint32)和互斥锁配合实现原子判断。它不是靠“加锁后检查再执行”这种常见模式,而是先用 atomic.LoadUint32 快速读取状态;只有当 done == 0 时,才抢锁并二次确认 —— 这避免了绝大多数场景下的锁竞争。
关键点在于:sync.Once 不关心你传进去的函数是否 panic。一旦函数返回(无论正常还是 panic),done 就会被设为 1,后续所有 Do 调用直接返回,不再尝试执行。
所以如果你的初始化函数可能 panic(比如配置加载失败、连接数据库超时),要提前在函数内部 recover,否则单例会处于“已标记完成但实际未就绪”的失效状态。
单例变量必须声明为指针类型
Go 中函数参数是值传递,sync.Once.Do 接收的是一个 func() 类型的值。如果单例结构体很大,或者你想在初始化后修改其字段,必须用指针。
立即学习“go语言免费学习笔记(深入)”;
常见错误写法:
var once sync.Once var instance Config // 值类型,Do 里赋值对全局 instance 无效
正确做法:
- 声明为
*Config指针变量 - 在
Do的回调中 new 或 & 生成地址并赋值 - 对外暴露的获取函数返回该指针,而非拷贝
初始化函数里别直接 return 实例值
sync.Once.Do 的参数签名是 func(),它不接收返回值。试图这样写会编译失败:
instance = once.Do(func() *DB { return NewDB() }) // ❌ 编译错误
必须把赋值逻辑写在函数体内:
var instance *DB
var once sync.Once
func GetDB() *DB {
once.Do(func() {
instance = NewDB() // ✅ 在闭包内赋值
})
return instance
}
注意:闭包捕获的是外部变量的地址,所以这里的 instance 赋值是有效的。但如果 instance 是局部变量(比如放在 GetDB 函数内),那就完全没意义 —— 每次调用都新建一次。
并发安全但不解决初始化依赖问题
sync.Once 只保证“某段代码最多执行一次”,它不管这段代码里做了什么。如果你的初始化函数依赖另一个单例,而那个单例也用 sync.Once 实现,就可能出现死锁:
比如 A 初始化需要 B,B 初始化又回头需要 A —— 两个 Do 同时阻塞在对方的锁上。
这类问题无法靠 sync.Once 自动解决,得靠设计规避:
- 拆分初始化逻辑,让依赖关系变成单向
- 把强依赖项作为参数传入,而不是在初始化函数里调用 GetXXX()
- 用 lazy-init + error 返回,让调用方决定重试或 fallback
真正容易被忽略的是:sync.Once 对象本身不能被复制。如果把它嵌进结构体并做值拷贝,每个副本都会有自己的 done 状态 —— 那就不是单例了。


















