sync.Once.Do不是简单加锁,因其采用原子状态机(uint32 done字段:0未执行、1执行中、2已完成),通过atomic.CompareAndSwapUint32争抢0→1跃迁,成功者执行函数,其余goroutine自旋等待至状态为2后直接返回,避免锁竞争开销。

sync.Once.Do 为什么不是简单加锁?
因为 sync.Once 的核心目标是「只执行一次」,且高并发下不能靠反复加锁抢执行权——那样会严重拖慢未命中路径的 goroutine。Go runtime 用的是原子状态机:内部 done 字段是 uint32,0 表示未执行,1 表示正在执行,2 表示已执行完毕。关键不是“锁”,而是用 atomic.CompareAndSwapUint32 去争抢从 0→1 的状态跃迁。
一旦某个 goroutine 成功 CAS 到 1,它就获得唯一执行权;其他 goroutine 看到状态是 1 或 2,就自旋等待,直到状态变成 2 后直接返回——全程不进锁竞争队列,避免调度开销。
Do 函数里为什么需要双重检查?
单纯靠 CAS 跳转状态还不够:CAS 成功(0→1)的 goroutine 要真正执行 f(),但执行期间可能 panic。Go 的实现会在 defer 中用 atomic.StoreUint32(&o.done, 0) 回滚状态,让下次调用能重试。这就引入了竞态风险:另一个 goroutine 可能在 f() 执行中、状态还是 1 时进入,然后等完发现 done 变成 0,又去 CAS……所以必须在 f() 执行前后都检查 atomic.LoadUint32(&o.done)。
- 第一次检查在 CAS 前:避免重复初始化(别人已成功写入 2)
- 第二次检查在 CAS 成功后、f() 执行前:防止被 panic 回滚后又被别的 goroutine 抢占
- f() 执行完再用 CAS 从 1→2,才算真正落地
为什么 sync.Once 不支持重置?
因为它的状态字段 done 是只增不减的 uint32,没有暴露任何修改接口,runtime 也不允许外部干预。设计上就是“一次性”的语义——哪怕 f() panic 了,只要它曾成功走到 atomic.StoreUint32(&o.done, 2),后续调用就永远跳过。如果业务真需要重试或重置,得自己封装一层带重试逻辑的结构体,或者换用 sync.OnceValue(Go 1.21+)配合手动管理。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
立即学习“go语言免费学习笔记(深入)”;
常见误用是试图通过反射或 unsafe 修改 done 字段,这不仅破坏内存安全,还可能让 runtime 的原子操作失效,引发不可预测的竞态或 crash。
sync.Once.Do 在什么场景下会卡住?
它本身不会卡住,但使用者会让它卡住:比如传入的函数 f() 里有死锁、无限循环、或阻塞在 channel 上没人接收。此时所有后续调用都会在 for atomic.LoadUint32(&o.done) == 1 { runtime.Gosched() } 循环里自旋等待——不是锁住了,而是傻等那个永远不结束的 f()。
- 典型陷阱:
once.Do(func() { ch ,而 ch 是无缓冲且没人读 - 更隐蔽的是 f() 里调用了另一个也依赖
sync.Once的函数,形成环状等待(虽少见,但可能) - 调试时注意看 goroutine stack:若大量 goroutine 停在
sync.(*Once).Do的 for 循环里,基本说明 f() 没出来
真正难察觉的是 panic 后的回滚逻辑——如果 f() panic 且没被 recover,sync.Once 会把状态打回 0,但 panic 本身会向上传播,外层若没处理,整个 goroutine 就结束了,看起来像“没执行”,其实是执行失败后被重置了。

















