sync.once专为单次初始化设计,非双重检查锁;其do方法通过原子读、cas和互斥锁实现快/竞态/慢三路径,确保函数仅同步执行一次,但不处理panic重试或异步完成。

直接用 sync.Once.Do,别自己加锁或用布尔标记——它专为这事设计,且线程安全、无竞态、不重复执行。
为什么不能用全局 bool + if 判断
看似简单,但多协程下必然出问题:多个 goroutine 同时读到 initialized == false,然后都进分支执行初始化逻辑。结果是资源被重复创建、配置被多次加载、甚至 panic。
- 即使加了
sync.Mutex,也容易漏锁、错锁范围,或在 panic 后忘记解锁 -
init()函数无法按需触发,必须在包加载时执行,不适合依赖运行时参数的初始化 -
sync.Once是标准库提供的零成本抽象,比手写逻辑更可靠
sync.Once.Do 的调用时机和参数限制
必须传入一个无参、无返回值的函数(func()),且该函数内部不能再调用任何 Do 方法——否则会死锁。
- 错误示例:
once.Do(func() { once.Do(anotherFunc) })→ 第二层Do会尝试获取同一把锁,而外层已持锁未释放,直接卡死 - 正确做法:把所有依赖初始化逻辑收拢到一个顶层函数里,或拆成独立
Once实例 - 如果初始化函数可能 panic,
sync.Once仍会将done标记为 1,后续调用直接跳过——这点要心里有数,panic 不等于“重试”
底层到底是怎么做到只执行一次的
靠的是「原子读 + 互斥锁 + 双重检查」三步组合,不是单纯靠锁排队。
- 第一次调用
Do时,先原子读o.done(初始为 0),命中则进doSlow -
doSlow加o.m.Lock(),再检查一遍o.done == 0(防其他 goroutine 已抢先完成) - 确认后才执行传入函数,并在
defer中原子写o.done = 1 - 后续所有调用都直接原子读到 1,跳过锁和函数执行——热路径几乎无开销
真正容易被忽略的是:一旦 Do 执行过(无论成功或 panic),就再也无法重置 sync.Once 实例。它不可复用、不可重试、不可重置——设计上就是“一锤定音”。如果初始化逻辑需要失败后重试,得自己在外层封装状态和重试逻辑,而不是指望 sync.Once。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











