sync.Once 仅保证初始化逻辑执行一次,不管理返回值生命周期;其 Do 方法无返回值,无法直接返回单例实例,需配合包级变量实现延迟初始化。

Go 的 sync.Once 确实能保证函数只执行一次,但它不是“单例对象”的封装工具——它只控制「某段初始化逻辑」的执行时机,不管理返回值的生命周期或复用方式。
为什么 sync.Once.Do 不能直接返回单例实例?
sync.Once.Do 的签名是 func (o *Once) Do(f func()),它只接受无参无返回值的函数。这意味着你无法靠它直接构造并返回一个对象(比如数据库连接、配置实例)。常见错误是试图在 Do 里赋值给包级变量,但忘了同步读取时的可见性问题。
- 包级变量必须是
var声明,且初始化后不可变(或确保线程安全) - 如果初始化函数里做了耗时操作(如 HTTP 请求),
Do会阻塞所有后续调用直到完成——这是设计使然,不是 bug - 不要在
Do里 panic:一旦 panic,Once会标记为已执行,后续调用不再触发,但也不会重试
典型用法:配合包级变量实现延迟初始化
最稳妥的做法是把实例存在包级变量里,用 sync.Once 控制其初始化时机。注意变量声明位置和读取顺序:
var (
dbInstance *sql.DB
dbOnce sync.Once
)
func GetDB() *sql.DB {
dbOnce.Do(func() {
db, err := sql.Open("mysql", "user:pass@tcp(127.0.0.1:3306)/test")
if err != nil {
panic(err) // 或 log.Fatal,但别忽略
}
dbInstance = db
})
return dbInstance
}
-
dbInstance必须是导出变量(首字母大写)或在同包内可访问,否则外部拿不到 - 返回前不加锁:因为
dbInstance是指针,且只写一次,后续读取天然安全(Go 内存模型保证写后读的可见性) - 不要在
Do外部对dbInstance做非原子写入(比如先 new 再赋值),这会破坏 once 语义
并发调用 sync.Once.Do 的行为细节
多个 goroutine 同时调用 Do 时,只有一个会执行传入函数,其余全部阻塞等待,直到该函数返回。这不是“竞态检测”,而是基于底层 atomic.CompareAndSwapUint32 的状态机实现。
- 即使函数执行时间很长(比如 5 秒),其他 goroutine 也会原地等待,不会超时或重试
- 如果初始化函数内部启动了 goroutine 异步干活,
Do仍会等它返回才放行——所以别把异步逻辑直接塞进Do函数体 -
Once结构体本身很小(仅含一个uint32字段),可嵌入结构体,但嵌入后不能共用同一个Do实例
真正容易被忽略的是:一旦 Do 执行过,就再也无法重置。如果你需要“失败后重试”或“按需重建”,sync.Once 不适用,得换用带状态管理的封装(比如用 atomic.Value + 自定义锁)。

















