sync.once.do只执行一次,因其用uint32原子变量done标记状态,首次成功调用后置为1,后续直接跳过;panic后也不重试,且多goroutine并发时仅一个执行、其余等待。

sync.Once.Do 是如何保证只执行一次的
核心靠一个 uint32 类型的 done 字段和原子操作。它不依赖锁的互斥,而是用 atomic.LoadUint32 先读状态,仅当未完成(done == 0)时才尝试用 atomic.CompareAndSwapUint32 抢占标记为“正在执行”,抢到的 goroutine 负责真正调用初始化函数,其余全部阻塞等待。
为什么不用 mutex 而用原子操作 + sync.Mutex 组合
单纯用 sync.Mutex 会带来不必要的竞争开销:即使初始化已完成,每次 Do 还得进锁再判断;而纯原子操作又无法安全等待——多个 goroutine 同时发现 done == 0,必须有一个执行、其余挂起并等它结束。所以 sync.Once 实际用了“双检查 + 一把懒加载的互斥锁”:
- 第一次调用且抢到 CAS 的 goroutine,会把内部
m(sync.Mutex)锁住,执行函数,再置done = 1 - 其他 goroutine 在 CAS 失败后,直接
m.Lock()等待,等第一个释放锁后,它们立刻拿到done == 1并返回 - 后续所有调用都走快速路径:
atomic.LoadUint32(&o.done) == 1,直接返回,零锁开销
Do 函数 panic 会导致什么后果
如果传入的初始化函数 panic,sync.Once 仍会将 done 置为 1,即标记“已执行完毕”。这意味着:
- panic 不会重试,也不会暴露给后续调用者
- 后续所有
Do调用都静默返回,不再执行函数,也不再 panic - 你无法从外部感知这次失败——除非在初始化函数里自行记录错误或设置全局标志
这是设计使然,不是 bug。Go 官方认为“一次性”意味着“至多一次”,无论成功或失败。
底层字段和内存布局是否稳定可依赖
不可依赖。sync.Once 的结构体定义是:
type Once struct {
done uint32
m Mutex
}
但文档明确说明:该结构体不应被复制,且其字段是未导出的实现细节。例如:
- Go 1.22 中
done仍是uint32,但未来可能改为uintptr或加入 padding - 有人曾通过
unsafe读done做“预检”,这属于未定义行为,会在某些架构或 GC 优化下出错 - 唯一安全用法只有
Once.Do(func()),其它任何对字段的访问或结构体拷贝都会破坏语义
真正容易被忽略的点是:哪怕你只是把 sync.Once 嵌入到另一个 struct 里,也必须确保该 struct 不被复制——否则副本里的 done 和 m 是全新独立的,完全失去“once”语义。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











