用 sync.once,别手写锁、别 double-check、别依赖 init()——它是 go 唯一被官方设计用来解决“只执行一次”问题的原语,因其内部用原子操作+互斥锁保证 happens-before 关系、支持懒加载、避免竞态且性能更优。

直接结论:用 sync.Once,别手写锁、别 double-check、别依赖 init() —— 它是 Go 唯一被官方设计用来解决“只执行一次”问题的原语。
为什么不能自己写双重检查锁(double-checked locking)
Go 的内存模型和编译器优化会让手写双重检查失效:即使加了 sync.Mutex,也可能因指令重排或缓存不一致,导致某个 goroutine 看到未完全初始化的 instance 指针,引发 nil pointer dereference 或数据竞态。
sync.Once 内部用原子操作 + 互斥锁组合,自动建立 happens-before 关系,保证初始化完成前所有字段写入对其他 goroutine 可见。
- 手写锁每次调用都得进临界区,性能差;
sync.Once.Do()首次有轻量锁开销,之后全是原子读 -
init()是包加载时执行,无法实现懒加载,也不支持参数化初始化 - 哪怕只读场景,也不能靠
if instance == nil判断后新建——这是竞态根源
sync.Once 必须是包级变量,不是结构体字段
常见误用是把 once sync.Once 声明在结构体里,或者在函数内 new 一个。这会导致每个实例/每次调用都有独立的 Once,完全失去“全局只一次”的意义。
once 和 instance 都必须是包级变量(如 var instance *DB; var once sync.Once)。
- 若单例属于某个结构体(比如
Service的内部连接),则once应作为该结构体字段,但必须确保该结构体本身是全局唯一的(比如也是包级变量) - 切勿在循环或闭包中声明
once或instance—— 它们会随函数作用域销毁或重复创建
sync.Once.Do() 里 panic 或出错怎么处理
sync.Once.Do() 函数签名是 func(f func()),不接受返回值,也不暴露 error。初始化失败时,error 会被静默吞掉,后续调用仍返回 nil 实例,极易导致运行时 panic 且无迹可寻。
- 必须用包级或结构体字段显式存错,比如
var initErr error或err error字段 - 赋值要原子:用
*error指针类型(initErr = &err)或直接赋值(initErr = err),Go 中error是接口,赋值本身是原子的 - 获取函数需主动检查:
if initErr != nil { return nil, initErr },不能只判instance == nil - 一旦
Do()中panic,sync.Once就标记为done,不会再重试——所以初始化前建议加日志,失败后做基础校验(如if instance == nil { panic("init failed") })
Go 1.21+ 推荐用 sync.OnceValue 替代手动存变量
传统 sync.Once 需要额外声明变量存结果,冗余且易错。sync.OnceValue 把“只执行一次”和结果缓存封装在一起,更简洁安全。
它返回一个函数,首次调用执行初始化逻辑并缓存结果,后续调用直接返回缓存值:
calculate := sync.OnceValue(func() int {
fmt.Println("Start complex calculation")
return 42
})
fmt.Println(calculate()) // 打印计算日志,返回 42
fmt.Println(calculate()) // 不再打印,直接返回 42
- 并发安全:多个 goroutine 同时调用
calculate(),也只执行一次初始化 - 错误处理用
sync.OnceValues,返回func() (T, error),适合配置加载、文件读取等可能失败的场景 - 注意:
OnceValue返回的是函数,不是值;不能用于需要提前知道类型或做类型断言的复杂初始化逻辑
真正容易被忽略的点是:错误处理必须显式落地,且 once 和 instance 的生命周期必须对齐——它们不是语法糖,而是语义契约。一旦放错作用域,或忽略失败路径,问题会在高并发下突然爆发,且难以复现。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











