sync.Once.Do不会重复执行,因其用原子整数done标记状态:首次CAS设为1并执行函数,后续直接返回;若函数panic则重置done,导致重复执行,需recover或前置校验。
sync.Once.Do 为什么不会重复执行
的核心不是靠锁“拦住后续调用”,而是靠一个原子整数
字段标记是否已完成。第一次调用
时,它先原子读取
;为 0 就尝试 CAS 设为 1,并真正执行传入的函数;之后再调用,
已是 1,直接返回,连锁都不用上。
这意味着:
只要传给的函数本身是无副作用或幂等的,整个机制就安全
。但反过来,如果函数内部有非线程安全操作(比如往全局 map 写数据却没加锁),
并不帮你兜底。
不要在
函数里做需要额外同步的共享状态修改
别把
当成通用互斥锁用——它只保“一次”,不保“排他”
接收的是
,不能传带参数的闭包(除非参数在闭包外已确定)
多次调用 Do 但函数仍执行了两次?检查 panic 场景
这是最常被忽略的坑:
在函数 panic 时,会重置状态,导致下一次调用再次执行
。Go 官方文档明确写了这点:一旦
panic,
视为未执行过。
典型现象:日志里看到初始化函数打印了两遍,或者数据库连接报 “already connected” 错误。
立即学习
“
go语言免费学习笔记(深入)
”;
务必在
传入的函数里用
捕获 panic(仅限你确认可恢复的场景)
更推荐做法:把可能 panic 的逻辑提前校验或封装,确保
内部函数本身不 panic
例如初始化配置时,先
检查文件是否存在,而不是直接
后不管错误
sync.Once 和 init 函数、包级变量初始化的区别
是编译期绑定的,只在包加载时运行一次,无法控制时机;
是运行时按需触发的,适合延迟初始化(比如首次 HTTP 请求才建连接池)。
关键差异在于生命周期和可见性:
无法传参、无法返回值、无法 retry;
可以配合函数参数(通过闭包捕获)、可结合 error 返回做失败重试逻辑
包级变量如
是包内共享的;但若定义在结构体里(
),那每次 new Client 都是独立实例,各自有自己的“一次”
跨 goroutine 安全:两者都安全,但
的“一次”是针对该实例的每一次调用,
是针对整个包的加载过程
Do 传函数时常见的闭包陷阱
写
看似没问题,但如果
和
是循环变量或后续会被修改的变量,实际执行时可能拿到错误值。
这是因为闭包捕获的是变量引用,不是快照。
正确做法:显式传值,
或者封装成工厂函数:
返回一个无参
避免在 for 循环里直接用循环变量构造闭包传给
复杂点在于:这种 bug 不一定立刻暴露,可能只在高并发或特定调度顺序下才出问题——所以只要用了闭包,就得多看一眼变量捕获逻辑。
sync.OncedoneDodonedoneDosync.OnceDosync.OnceDofunc()sync.Once.DofOnceDodefer/recoverDoos.Statos.Openinitsync.Onceinitsync.Oncevar once sync.Oncetype Client struct { once sync.Once }sync.Onceinitonce.Do(func() { initDB(host, port) })hostporthost, port := host, port; once.Do(func() { initDB(host, port) })newInitializer(host, port)func()Do