sync.once 是 go 中实现单例最推荐的方式,它通过 do 方法确保初始化函数仅执行一次。使用时定义一个 once 实例和单例变量,在 getinstance 函数中调用 once.do 来初始化对象,保证并发安全、代码简洁且性能良好。常见误区包括传参错误和复用 once 对象,此外还有全局变量、init 函数和加锁等其他单例实现方式,但均不如 sync.once 安全高效。

单例模式在Go语言中很常见,尤其是在需要确保某个对象只被初始化一次的场景下。实现方式有很多种,但最推荐、最安全的方式是使用标准库中的
sync.Once。

sync.Once 是什么?
sync.Once是 Go 标准库提供的一个工具结构体,它保证某个函数只会被执行一次,即使在并发环境下也能线程安全地完成初始化工作。它的定义很简单:

type Once struct {
// contains filtered or unexported fields
}它只有一个方法
Do(f func()),传入的函数
f只会被执行一次,后续调用都会被忽略。
立即学习“go语言免费学习笔记(深入)”;
这正好符合单例模式的需求:实例只创建一次,多次获取都是同一个对象。

如何用 sync.Once 实现单例?
实现起来非常简单,基本结构如下:
type Singleton struct{}
var instance *Singleton
var once sync.Once
func GetInstance() *Singleton {
once.Do(func() {
instance = &Singleton{}
})
return instance
}在这个例子中,无论多少个 goroutine 同时调用
GetInstance(),
once.Do都能保证内部的匿名函数只执行一次,从而确保
instance只被初始化一次。
这种方式的优点很明显:
- 线程安全,不需要自己加锁
- 代码简洁清晰,逻辑直观
- 性能好,不会每次都加锁判断
常见误区和注意事项
虽然
sync.Once很方便,但在使用过程中也有一些容易踩坑的地方:
✅ 必须传入无参数函数
once.Do()接受的是一个没有参数的函数,所以如果你的初始化过程需要参数,那就要在闭包里处理清楚,不能直接传参进去。
例如这样是可以的:
once.Do(func() {
instance = NewSingletonWithConfig(cfg)
})但不要试图把
Do当成可以传参执行的函数,它不是设计用来做这个的。
❌ 不要重复使用 Once 对象
每个
sync.Once只应该用于一个初始化动作。如果你在一个结构体里复用了同一个
Once对象去做多个初始化,可能会导致混乱。
比如下面这种写法就有问题:
var once sync.Once
func InitA() { once.Do(doA) }
func InitB() { once.Do(doB) } // 错误!InitA执行后,once已经触发过了一旦其中一个初始化完成,另一个就永远不会再执行了。
单例还可以怎么写?为什么推荐 Once?
除了
sync.Once,还有几种常见的单例写法:
全局变量直接初始化
比如var instance = &Singleton{}
这种方式最简单,但不适合需要延迟加载或有复杂初始化逻辑的情况。使用 init 函数初始化
利用包级init()
函数来初始化单例
优点是自动执行,缺点是控制力差,无法按需加载。加锁实现(比如 Mutex)
在并发访问时加锁判断是否已初始化
虽然可行,但每次都要加锁判断,性能不如Once
干净利落。
相比之下,
sync.Once的优势在于:
- 安全性高:官方封装,经过验证
- 性能好:只在第一次加锁,之后无开销
- 语义清晰:一看就知道是用来做“只执行一次”的任务
小细节补充
once.Do()
中如果函数 panic 了,Once 会认为已经执行过,后续调用也不会再执行。- 如果你希望即使失败也重试,就不能用 Once,得自己控制状态。
- Once 本身是可复制的吗?理论上不应该复制,因为其内部状态可能出错。最好作为包级变量或者结构体内嵌使用。
基本上就这些。用
sync.Once实现单例是 Go 中最推荐的方式,不复杂但确实实用。










