Once

Once #

Go 标准库中 sync.Once 可以保证 Go 程序运行期间的某段代码只会执行一次。常常用于单例对象的初始化场景。

使用 #

sync.Once 只有一个对外唯一暴露的方法 Do,可以多次调用,但是只第一次调用时会执行一次。

func main() {
    o := &sync.Once{}
    for i := 0; i < 10; i++ {
        o.Do(func() {
            fmt.Println("only once")
        })
    }
}

运行:

$ go run main.go
only once

利用 channel 实现 Once #

下面的代码也可以达到执行一次的效果,不过重复执行会导致 panic:

var setonce chan struct{}

func initialize() {
	// channel 不可以重复关闭,否则会 panic
    close(a.setonce)
	// 初始化
	// ...
}

原理 #

sync.Once 的实现:

// src/sync/once.go
type Once struct {
	done uint32
	m    Mutex
}

func (o *Once) Do(f func()) {
	// 如果传入的参数 f 已经执行过,直接返回
    if atomic.LoadUint32(&o.done) == 0 {
        o.doSlow(f)
    }
}

func (o *Once) doSlow(f func()) {
	// 为当前 goroutine 加锁
    o.m.Lock()
    defer o.m.Unlock()
    if o.done == 0 {
		// 将 done 设置为 1
        defer atomic.StoreUint32(&o.done, 1)
		// 执行参数 f
        f()
    }
}

sync.Once 使用互斥锁和原子操作实现了某个函数在程序运行期间只能执行一次的语义。

使用互斥锁,同时利用双检查的机制(double-checking),再次判断 o.done 是否为 0,如果为 0,则是第一次执行,执行完毕后,就将 o.done 设置为 1,然后释放锁。

即使有多个 goroutine 同时进入了 doSlow 方法,因为双检查的机制,后续的 goroutine 会看到 o.done 的值为 1,也不会再次执行 f