不能直接用全局变量加init函数,因init仅在包加载时执行一次,无法按需初始化(如依赖未就绪的配置或db),且多goroutine并发调用时if判空不安全;sync.once通过原子状态机+cas+互斥锁确保do内函数仅执行一次;sync.oncevalue(go 1.21+)更简洁但不可处理错误,失败即panic。

为什么不能直接用全局变量加 init 函数?
因为 init 只在包加载时执行一次,无法控制单例的**按需初始化时机**——比如依赖外部配置、数据库连接或环境变量,这些可能在 init 阶段尚未就绪。强行提前初始化容易 panic 或返回 nil,且无法重试。
更关键的是:多个 goroutine 同时首次调用该函数时,必须保证只执行一次初始化逻辑,否则会竞态。单纯靠 if instance == nil 判断 + 赋值是不安全的。
sync.Once 是怎么解决并发初始化问题的?
sync.Once 内部用原子状态机(uint32)标记是否已执行,配合 atomic.CompareAndSwapUint32 和互斥锁兜底,确保 Do 中的函数最多被执行一次,且所有调用者都会等待它完成。
注意:sync.Once 不可重置,也不支持传参;它的作用就是「封住一次执行」,不是通用懒加载工具。
典型用法:
var once sync.Once
var instance *MyService
<p>func GetService() *MyService {
once.Do(func() {
instance = &MyService{...} // 初始化逻辑放这里
})
return instance
}</p>
常见误用:把耗时操作或错误处理塞进 Do 里却不暴露失败信号
如果初始化可能失败(比如打开文件、连 DB),sync.Once.Do 本身不返回 error,也没法重试。一旦失败,后续所有调用都拿到 nil 或半初始化对象。
- 别这样写:
once.Do(func() { instance, _ = NewDBClient(...) })—— 错误被吞掉 - 正确做法是把初始化封装成带 error 的函数,并用额外变量记录状态:
var (
once sync.Once
instance *DBClient
errOnce error
)
<p>func GetDBClient() (*DBClient, error) {
once.Do(func() {
instance, errOnce = NewDBClient()
})
return instance, errOnce
}</p>
替代方案:为什么有时候该用 sync.OnceValue(Go 1.21+)?
如果你的初始化函数返回值类型固定、无副作用、且希望避免手动管理 error 变量,sync.OnceValue 更简洁安全。它返回 func() T,内部自动缓存结果并保证并发安全。
但它要求初始化函数不能有参数、不能返回多个值(只能一个),且无法区分「未执行」和「执行失败」——失败时 panic,所以仍需在初始化函数内做 recover 或提前校验。
示例:
var clientOnce = sync.OnceValue(func() *http.Client {
return &http.Client{Timeout: 30 * time.Second}
})
<p>func GetHTTPClient() *http.Client {
return clientOnce()
}</p>
真正难的不是选 sync.Once 还是 sync.OnceValue,而是想清楚:初始化失败要不要重试?要不要区分 transient error 和 fatal error?这些逻辑没法靠并发原语自动解决,得自己兜住。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











