不能直接用全局变量+init函数做单例,因为init执行时机不可控且无法延迟初始化,高成本构造可能白白浪费资源;sync.Once.Do通过原子标志位保证只执行一次,支持安全懒加载。

为什么不能直接用全局变量+init函数做单例
很多人写单例第一反应是声明一个包级变量,再在 init() 里初始化。这看似简单,但问题在于:如果单例对象的构造成本高(比如要连数据库、读大文件、启动子进程),而实际运行中根本没用到它,就白白浪费资源。更麻烦的是,init() 执行时机不可控,无法按需延迟初始化。
Sync.Once.Do 是怎么保证“只执行一次”的
sync.Once 内部用一个 uint32 原子标志位控制状态,配合 sync/atomic 的 LoadUint32 和 CompareAndSwapUint32 实现无锁快速判断。只有第一次调用 Do 时才会真正执行传入的函数;后续调用直接返回,不阻塞也不重复执行。
注意:Do 的参数必须是 func() 类型,不能带参数或返回值。所以你要把初始化逻辑封装成闭包,把依赖项提前捕获进去:
var once sync.Once
var instance *DB
func GetDB() *DB {
once.Do(func() {
instance = &DB{conn: connectToDB()} // 这里可以调用任何耗时操作
})
return instance
}
常见误用:在 Do 里返回错误或忽略 panic
因为 sync.Once.Do 不接受返回值,所有初始化失败都只能靠副作用暴露——比如设一个包级错误变量,或者直接 panic。但 panic 在并发场景下容易掩盖真实问题。
在 Golang 中使用 samber/hot 进行内存缓存,支持 LRU、LFU、TinyLFU、W‑TinyLFU、S3FIFO、ARC、TwoQueue、SIEVE、FIFO 等淘汰算法,提供 TTL、缓存加载器及分片功能。
立即学习“go语言免费学习笔记(深入)”;
- 不要在
Do闭包里用log.Fatal或os.Exit,会直接终止整个进程 - 推荐方式:定义一个
errOnce sync.Once+initErr error,在Do里赋值,然后在GetDB返回前检查initErr != nil - 如果初始化函数本身可能 panic(比如未处理的空指针解引用),
Do会把它传播出去,调用方必须能 recover
和 double-checked locking 相比,Once 更轻量且不易出错
有人会想手写双重检查(先查实例是否为 nil,再加锁,再查一次),但这在 Go 里没必要。Go 的内存模型对 sync.Once 有特殊保证:一旦 Do 返回,其内部函数中写入的所有变量,对后续所有 goroutine 都是可见的——不需要额外的 sync.Mutex 或 atomic.StorePointer。
换句话说,你不用操心指令重排、缓存不一致这些问题。只要用对了 sync.Once,就能安全地实现线程安全的懒加载单例。
真正容易被忽略的是:Once 对象本身不能复制。如果把它嵌进结构体并做了值拷贝,每个副本都会有自己的标志位,那就不是“once”了。务必确保 sync.Once 字段是地址传递或包级变量。

















