Golang中Sync.Once与单例初始化方式的对比
Golang中Sync.Once与单例初始化方式的对比 我需要获取数据库的单例实例。为此,我编写了以下代码:
func getDb() map[string]int {
if db == nil {
lock.Lock()
if db == nil {
db = map[string]int{}
}
lock.Unlock()
}
return db
}
还有另一种使用 sync.Once 的方法:
func (o *Once) Do(f func()) {
if atomic.LoadUint32(&o.done) == 0 {
o.doSlow(f)
}
}
func (o *Once) doSlow(f func()) {
o.m.Lock()
defer o.m.Unlock()
if o.done == 0 {
defer atomic.StoreUint32(&o.done, 1)
f()
}
}
我应该使用哪一种?
atomic.LoadUint32(&o.done) == 0 的计算成本是否比 db == nil 更高?
更多关于Golang中Sync.Once与单例初始化方式的对比的实战教程也可以访问 https://www.itying.com/category-94-b0.html
非常感谢大家,
我已经得到了答案。 
更多关于Golang中Sync.Once与单例初始化方式的对比的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
这两个示例做的事情并不相同。一个是现成的解决方案,而第一个示例是手工实现的。如果需要多次使用……或许使用 Once 更好。
嗯,如果130纳秒对你的用例来说很关键……那就用第一个方案,否则我倾向于使用“现成”的解决方案;但这只是我的个人想法。
但是,基准测试工具却给出了不同的结果 -

基准测试工具显示两者性能相近
你好 @Harshit_Soni,
使用 sync.Once 来创建单例,这是符合 Go 语言习惯的做法。
你为什么要关心这种简单且很少使用的操作有多快?通常在你的应用程序中,你只需要创建一次数据库连接,为什么要在意它是否多花了 10% 的时间?如果代码更易于阅读(和维护),通常值得选择性能稍差的算法。
你的基准测试只显示了非常微小的差异,如果运行更长时间,甚至可能显示相反的结果。选择 sync.Once 似乎是一个简单的决定。
嗯,如果对于你的使用场景来说,130纳秒至关重要……那就用第一个解决方案,否则我倾向于使用“开箱即用”的解决方案;但这只是我的想法。
确实。如果我没记错的话,我们讨论的是0.00000013秒,而这项操作根据定义只会执行一次。根据具体的使用场景,这或许也是一个适合使用init函数的候选方案。这样你就可以完全排除并发问题,这个db属性在你的应用启动时就已就绪。
原子操作是底层内存原语,根据我的使用和理解,它们的性能相当不错。我在想,那个微小的性能差距是否是由defer o.m.Unlock()引起的?也可能是因为使用了多个函数/闭包而产生了极小的开销。无论如何,这都无关紧要,因为我可以向你保证,连接到数据库并进行身份验证所花费的时间,将比你使用sync.Once可能带来的任何性能损失高出几个数量级。
在Go中,sync.Once是专门为单次初始化设计的标准库方案,相比手动实现的双重检查锁(DCL)更可靠。以下是具体分析:
1. 正确性对比
你的DCL实现存在数据竞争问题:db == nil的第一次检查没有加锁,多个goroutine可能同时通过检查,导致多次初始化。虽然后续加锁检查能防止重复赋值,但竞态条件可能引发未定义行为。sync.Once通过原子操作和互斥锁的组合保证了绝对的单次执行。
2. 性能分析
atomic.LoadUint32的成本与db == nil相当,都是单次内存读取。但sync.Once在初始化完成后会通过原子加载快速路径返回,避免锁开销。示例:
var (
db map[string]int
once sync.Once
)
func getDb() map[string]int {
once.Do(func() {
db = make(map[string]int)
// 初始化代码...
})
return db
}
3. 内存顺序保证
sync.Once使用defer atomic.StoreUint32(&o.done, 1)确保初始化函数对内存的修改对其他goroutine可见,这是手动实现容易忽略的细节。
4. 错误处理
sync.Once确保即使初始化函数panic,也标记为已执行,避免重复执行失败代码:
var config *Config
var once sync.Once
func loadConfig() {
once.Do(func() {
config = &Config{}
if err := config.Load(); err != nil {
panic(err) // 后续调用不会重复执行
}
})
}
结论:始终使用sync.Once。它是标准库经过验证的解决方案,避免了手动实现DCL时容易出现的竞态条件和内存可见性问题。性能上两者差异可以忽略,但正确性上有本质区别。

