跳转到主内容
极星编程网:以代码为星,赴技术山海!

Golang sync.Once怎么保证只执行一次_Golang Once单次执行教程【总结】

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

相关文章