sync.Once 仅保证函数执行一次,不解决初始化成功与否;需配合包级指针变量、错误记录和原子字段优化使用,且多步初始化必须合并到单个闭包中。

sync.Once 本身不实现单例,它只保证函数执行一次;单例是否成立,取决于你如何用它配合包级变量、指针和错误处理。
为什么 sync.Once.Do 不能直接返回实例
sync.Once.Do 的函数签名是 func(),它不接收返回值,也不透出错误。试图在 Do 调用里赋值并期望返回,会编译失败:
instance = once.Do(func() *DB { return NewDB() }) // ❌ 编译错误
正确做法是把初始化逻辑和赋值写在闭包内部,靠闭包捕获外部指针变量:
- 声明包级变量为指针类型(如
var instance *DB) - 在
Do的匿名函数中执行instance = NewDB() - 对外暴露的获取函数直接返回该指针
sync.Once 初始化失败时,实例可能为 nil
如果 NewDB() 内部 panic 或返回 nil(比如数据库连接超时),sync.Once 仍会将 done 置为 1,后续调用永远返回这个无效值 —— 调用方拿到 nil 后 panic,但无法重试。
解决方式不是绕开 sync.Once,而是把错误处理收进闭包:
立即学习“go语言免费学习笔记(深入)”;
- 用额外包级变量记录错误,如
var configErr error - 在
Do中完整执行初始化 + 错误检查,确保instance非 nil 或明确设错 - 获取函数返回
(*Config, error),而非裸指针
多个初始化步骤必须合并到一个 func() 里
sync.Once 只对单个函数生效。如果你拆成几步:
once.Do(initDB)<br>once.Do(initCache)<br>once.Do(initLogger)
这其实是三个独立的 Once 实例,彼此无序、不可控。真正需要“整体只执行一次”的初始化链,必须塞进同一个闭包:
- 所有依赖顺序的操作(如先连 DB,再建缓存 client,最后加载配置)写在一个函数里
- 避免在闭包中调用另一个
once.Do,否则会死锁 - 若某步失败,整个初始化失败,
done已标记,需靠外部机制(如重启进程或手动重置变量)恢复
性能关键点:done 字段放在结构体首位
sync.Once 内部的 done uint32 是第一个字段,这是有深意的 —— 热路径(hot path)上只需原子读取它,无需计算结构体偏移。绝大多数调用走的是 atomic.LoadUint32(&o.done) 这条快速通路,几乎不触发锁。
这意味着:
- 高并发下
Do调用开销极低,适合高频访问场景 - 但一旦进入慢路径(
done == 0),就会抢锁 + 二次检查 + 执行函数,此时所有等待 goroutine 会阻塞 - 所以别在
Do里做耗时操作(如未设 timeout 的 HTTP 请求),否则拖垮整个服务
sync.Once 不是万能初始化器,它只解决“一次”,不解决“成功”。


















