sync.Once仅保证同一实例的Do函数最多执行一次;误用如函数内声明新实例、Do中panic或需重试/传参/超时等场景均不适用,应改用sync.Map、context或自定义封装。

sync.Once 本身不保证“只执行一次”,它只保证 Do 方法传入的函数最多执行一次 —— 前提是你用的是同一个 sync.Once 实例,且没有在函数内部 panic。
为什么多次调用 Do 仍可能执行多次?
常见误用是把 sync.Once 声明在函数内部或每次构造新实例:
func bad() {
var once sync.Once
once.Do(func() { fmt.Println("run") }) // 每次都新建 once,每次都执行
}
正确做法是将其声明为包级变量、结构体字段,或通过依赖注入传入。否则它就跟没加锁一样。
- 包级初始化:用
var once sync.Once放在全局作用域 - 结构体字段:适用于需要按实例隔离的场景(比如每个连接对象只初始化一次其资源)
- 不要在循环、HTTP handler 或 goroutine 内部重复声明
sync.Once变量
Do 函数里 panic 会导致什么?
如果传给 Do 的函数 panic,sync.Once 会认为“已执行完成”,后续调用不再触发,但也不会恢复 panic —— panic 会继续向上抛出。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
立即学习“go语言免费学习笔记(深入)”;
这意味着:
- 第一次调用
Dopanic → 后续所有Do调用直接返回,不重试 - 没有内置重试或状态重置机制(
sync.Once没有公开 API 重置状态) - 若需失败后重试,得自己封装逻辑,比如配合
atomic.Bool+ 循环判断
替代方案:什么时候不该用 sync.Once?
它只适合“纯初始化”类场景,不适合带参数、需多次配置、或依赖外部状态变化的操作。
- 需要根据参数做不同初始化?→ 用
sync.Map或map[Key]*sync.Once(注意 key 类型要可比较) - 初始化过程可能被取消或超时?→
sync.Once不支持上下文,得换errgroup.Group或手动加context.Context控制 - 初始化结果要返回且可能失败?→ 把返回值和 error 封装进闭包,别指望
Do返回任何东西(它的签名是func(f func()),无返回)
最常被忽略的一点:sync.Once 的零值是有效的,不需要显式初始化;但一旦你把它嵌进 struct 并用指针接收方法,就得确保 struct 实例本身不是每次新建的 —— 否则又回到第一个坑。

















