开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 21 天,点击查看活动详情
Once
Once类型可以确保函数只被调用一次。Once类型有一个Do方法,Do方法接收一个无参函数作为参数,并且确保该函数只被执行一次,无论在任何时候被调用。在第一次调用Do方法时,会执行这个函数,以后的调用会直接返回,不会再次执行函数。Once类型可以用于一些只需要进行一次初始化的操作,比如单例模式等。
以下是一个使用Once类型的例子:
var once sync.Once
func main() {
// Do some initialization work, but only once.
once.Do(func() {
fmt.Println("Initializing...")
})
// Now, this line will not print anything, because the initialization work is already done.
once.Do(func() {
fmt.Println("This should not print.")
})
}
Once的错误实现
一个简单的想法是用一个变量标记函数是否执行过,再利用原子操作修改这个变量就行,如果执行过直接返回:
type Once struct {
done uint32
}
func (o *Once) Do(f func()) {
if !atomic.CompareAndSwapUint32(&o.done, 0, 1) {
return
}
f()
}
但是这是官方指出的错误实现方法,因为当函数执行很慢时,如果并发调用,例如初始化资源,会得到空值。Do应该保证当它返回时,f已经完成。
源码解析
一个正确的 Once 实现要使用一个互斥锁,这样初始化的时候如果有并发的 goroutine,就会进入doSlow 方法。
type Once struct {
done uint32
m Mutex
}
func (o *Once) Do(f func()) {
if atomic.LoadUint32(&o.done) == 0 {
// inline 加速
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()
}
}
- done:是否执行过的标志
- m:互斥锁,辅助确保执行一次
Do:
- 执行过就直接退出
- 没执行过就走doSlow
doSlow:
- 先加锁,双检查,执行f,原子修改标志,解锁
双检查的作用:
- 当函数执行时,Do会进入doSlow并阻塞,当函数执行完,解锁后,这些阻塞的doSlow还是会进入,要再检查一下是不是执行过,没有执行过才再执行。当然标准改完以后再执行的Do就不会进入doSlow了,double check主要是应对f执行时的do操作。