sync.Once.Do只执行一次,因其用uint32原子变量标记状态,首次CAS成功者执行函数,其余goroutine等待其完成但不重复执行;panic不被捕获,标记仍置为1,后续调用直接返回。

sync.Once.Do 为什么只执行一次
因为 sync.Once 内部用一个 uint32 原子变量标记是否已执行,Do 方法在调用时先原子读取该标记,为 0 才尝试 CAS 设置为 1 并执行传入函数;一旦设为 1,后续所有调用都跳过函数执行。这不是靠锁阻塞排队,而是靠原子状态跃迁实现的快速短路。
常见错误是误以为 Do 会等待前一次执行完成:如果多个 goroutine 同时首次调用 Do,只有一个能抢到执行权,其余会阻塞直到那个 goroutine 执行完函数——但它们不会重复执行,只是等结果。这容易被当成“并发安全的懒加载”,其实它更接近“一次性初始化门闩”。
传给 sync.Once.Do 的函数不能 panic
sync.Once 不捕获 panic。一旦传入的函数 panic,Once 的内部标记仍会被设为已执行(即原子值变为 1),后续调用直接返回,不再重试。这意味着:初始化失败后无法恢复,整个程序可能处于未定义状态。
实操建议:
- 在传给
Do的函数里手动 recover,把错误转为返回值或日志,避免 panic 泄漏 - 不要依赖
Do自动重试——它不重试,也不提供错误透出机制 - 若初始化逻辑可能失败(如加载配置、连数据库),应在外层封装重试逻辑或兜底策略,而不是交给
Once
sync.Once 和 init 函数的区别在哪
init 是包级静态初始化,在 main 启动前强制执行,不可控、不可延迟、不支持参数、无法按需触发;sync.Once 是运行时按需触发的单次执行,支持带上下文的初始化(比如根据环境变量决定加载哪套配置),且可跨包复用。
典型使用场景:
- 全局 logger 实例化(需读取配置文件,不能在
init里做 I/O) - 第三方 SDK 客户端的懒连接(如
redis.Client或http.Client首次使用时才建立连接池) - 避免在
init中调用其他包的未初始化变量(循环依赖风险)
注意:sync.Once 不替代 init,它替代的是“手写双重检查锁 + volatile 标记”的老式单例模式。
多个 sync.Once 实例之间没有同步关系
每个 sync.Once 变量独立维护自己的执行状态。如果你有多个字段需要协同初始化(比如 db 和 cache 必须一起建好才能用),不能分别用两个 Once 包裹各自初始化逻辑——它们之间无序,可能造成竞态或部分初始化成功。
正确做法:
- 把关联初始化逻辑合并进同一个
Do调用中 - 或者用一个
sync.Once控制入口,内部统一协调多个资源的构建与校验 - 别为了“解耦”而拆分
Once,那会破坏初始化的原子性
最容易被忽略的是:初始化函数里调用其他也依赖 Once 的组件时,要小心死锁——比如 A 的 Do 里调用了 B 的 Do,而 B 的 Do 又反过来依赖 A 的某个字段,就可能卡住。


















