sync.Once.Do 通过原子变量 done 和 CompareAndSwapUint32 实现无锁“最多执行一次”,不保证函数线程安全,不处理 panic,不可重试或重置,多 goroutine 会阻塞等待首次执行完成。

sync.Once.Do 为什么能确保函数只执行一次
sync.Once 内部用一个 uint32 原子变量(done)标记是否已执行,配合 sync/atomic.CompareAndSwapUint32 实现无锁判断。只有第一次调用 Do 时能成功将 done 从 0 改为 1,后续调用直接返回,不进临界区。
关键点在于:它不保证传入的函数是“线程安全”的,只保证“最多执行一次”——如果函数本身有竞态(比如修改全局变量但没加锁),sync.Once 不会帮你修复。
Do 方法传入的函数不能有参数或返回值
sync.Once.Do 的签名是 func(f func()),只能接受无参无返回值的函数。常见错误是试图直接传带参函数:
once.Do(func() { initDB("prod") }) // ✅ 正确:闭包捕获参数
once.Do(initDB("prod")) // ❌ 错误:立即执行,传的是 initDB 返回值(类型不匹配)
容易踩的坑:
立即学习“go语言免费学习笔记(深入)”;
- 闭包里引用的变量如果在
Do调用后被修改,可能引发意料外行为(比如闭包捕获了未初始化的指针) - 不要在
Do里调用可能 panic 的函数,除非你已处理——sync.Once不恢复 panic,一旦 panic,该Once实例永久失效(done不会回滚)
多个 goroutine 并发调用 Do 时的行为
所有 goroutine 会阻塞等待第一次调用的函数执行完毕,之后才统一返回。这意味着:
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 执行函数的 goroutine 是“胜出者”,其他 goroutine 不会重复执行,也不再抢夺执行权
- 如果初始化函数耗时较长(比如加载大配置、连数据库),所有并发调用者都会卡住,直到它结束
- 没有超时机制,无法取消;若初始化失败,也没有重试入口——必须靠上层设计兜底(比如用额外标志位 + 锁控制重试)
示例场景:全局 logger 初始化,多个模块同时首次调用 GetLogger(),都走 once.Do(initLogger),仅第一个真正初始化,其余等待并复用结果。
Once 实例不能复用或重置
sync.Once 是一次性结构体,字段 done 是 uint32,没有公开方法清零。一旦 Do 执行过,就不可逆。
如果你需要“可重试的一次性操作”,不能靠新建 sync.Once 实例来“重置”——因为旧实例状态已定,新实例和旧实例完全无关,无法替代原语义。
更现实的做法是:
- 用
sync.Once+ 额外的error变量记录失败状态,上层按需重建实例(但注意:这已不是“同一个 Once”的重试) - 改用
sync.Mutex+ 显式标志位,自己控制重试逻辑 - 避免在初始化函数里做不可逆且易失败的操作(比如写文件、发网络请求),优先拆分为可检查、可重试的步骤
最常被忽略的是:开发者以为 sync.Once 能兜底所有初始化失败场景,其实它只解决“执行次数”,不解决“执行成败”。失败后的恢复,得自己设计。

















