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

sync.Once.Do 为什么只执行一次
因为 sync.Once 内部用一个 uint32 原子变量记录是否已执行,Do 方法在调用前会用 atomic.LoadUint32 检查,只有值为 0 才尝试用 atomic.CompareAndSwapUint32 置为 1 并真正执行函数;一旦置为 1,后续所有调用都跳过函数体。
常见错误现象:Do 传入的函数里 panic 了,但后续再调用 Do 还是不执行——这是设计行为,不是 bug。panic 后 once 的状态已被设为“已完成”,不会重试。
- 不要在
Do函数里依赖外部可变状态做条件判断,它只跑一次,不管状态后来怎么变 - 如果需要“失败后重试”,得自己封装逻辑,
sync.Once不提供重置或重试接口 - 多个 goroutine 同时首次调用
Do,只有一个能进函数体,其余阻塞等待其返回(无论成功或 panic)
sync.Once 和 init() 函数的区别在哪
init() 在包加载时执行,仅一次,且无并发安全问题,但它无法延迟到运行时按需触发;sync.Once 是运行时、按需、并发安全的单次执行机制。
使用场景差异:
- 初始化数据库连接池?用
sync.Once,因为可能根本用不到,或者要等配置加载完 - 注册全局 HTTP handler?用
init()更合适,只要包被导入就该注册 - 需要根据环境变量决定是否初始化某服务?只能用
sync.Once,init()无法读取运行时环境
性能影响:两者开销都很小,但 sync.Once 首次调用有原子操作+可能的 mutex 锁竞争,init() 是纯编译期绑定,零运行时成本。
sync.Once.Do 传函数时常见的坑
最常踩的坑是传了带参数的闭包却没意识到捕获的是变量地址,导致所有调用共享同一份变量值。
错误写法示例:
var once sync.Once for i := 0; i <p>正确做法是立即求值或显式传参:</p>
- 用参数传入:
once.Do(func(v int) { fmt.Println(v) }(i)) - 或在外层用新变量绑定:
val := i; once.Do(func() { fmt.Println(val) }) - 更推荐:把初始化逻辑单独抽成函数,避免闭包陷阱
另一个坑:传入函数里启动 goroutine 但没等它结束,就认为“初始化完成”——Do 只等函数返回,不等它 spawn 的 goroutine。
多个 sync.Once 能否组合成“多阶段单次初始化”
不能直接组合,sync.Once 本身不支持阶段、依赖或取消。但可以用多个独立实例分别控制不同阶段,靠代码顺序和状态变量协调。
例如实现“先连配置中心,再初始化缓存”:
- 定义两个
sync.Once实例:loadConfigOnce和initCacheOnce -
initCacheOnce.Do里先调用loadConfigOnce.Do(...),再执行缓存初始化 - 注意:这仍是线性依赖,不是并发安全的“多阶段图”,如果有循环依赖或并发修改依赖关系,就得换方案(比如用
sync.RWMutex+ 状态字段)
容易被忽略的一点:多个 sync.Once 实例之间没有内存屏障自动同步,如果某个阶段写了全局变量,其他阶段读它,要确保读操作发生在 Do 返回之后——而这是由 Go 的 happen-before 规则保证的,只要别绕过 Do 直接读写。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











