在 go 语言中我们可以使用 sync.Once 对象来实现函数方法只执行一次的功能。

简单代码示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package main

import (
	"fmt"
	"sync"
)

func main() {
	var (
		o  sync.Once
		wg sync.WaitGroup
	)

	for i := 0; i < 10; i++ {
		wg.Add(1)

		go func(i int) {
			defer wg.Done()
			o.Do(func() {
				fmt.Printf("hello %d\n", i)
			})
		}(i)
	}

	wg.Wait()
}

输出:

1
hello 9

不使用 Sync.Once 的结果如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
hello 9
hello 4
hello 0
hello 1
hello 2
hello 3
hello 6
hello 5
hello 7
hello 8

可以看到, 在使用 sync.Once 的情况下, 只执行一次函数。

解析

通过查看源码,可以看到 Sync.Once 的源码十分简单。 只有一个结构体和2个方法。

1
2
3
4
type Once struct {
	done uint32
	m    Mutex
}

done 成员用来判断函数是否执行过。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
func (o *Once) Do(f func()) {
	// Note: Here is an incorrect implementation of Do:
	//
	//	if atomic.CompareAndSwapUint32(&o.done, 0, 1) {
	//		f()
	//	}
	//
	// Do guarantees that when it returns, f has finished.
	// This implementation would not implement that guarantee:
	// given two simultaneous calls, the winner of the cas would
	// call f, and the second would return immediately, without
	// waiting for the first's call to f to complete.
	// This is why the slow path falls back to a mutex, and why
	// the atomic.StoreUint32 must be delayed until after f returns.

	if atomic.LoadUint32(&o.done) == 0 {
		// Outlined slow-path to allow inlining of the fast-path.
		o.doSlow(f)
	}
}

Do 方法的实现是这样的,检查 done 成员,如果为 0,则执行 f 函数。

接下来看看 doSlow 方法,它是一个锁的实现,它的实现如下:

1
2
3
4
5
6
7
8
func (o *Once) doSlow(f func()) {
	o.m.Lock()
	defer o.m.Unlock()
	if o.done == 0 {
		defer atomic.StoreUint32(&o.done, 1)
		f()
	}
}

doSlow 会执行函数 f,并且在执行完 f 后将 done 成员设置为 1。 在 doSlow 当中使用了互斥锁来保证只会执行一次

小结

Once 保证了传入的函数只会执行一次,这常用在这些场景下:单例模式,配置文件加载,初始化。

Once 是不能复用的,只要执行过一个函数,其他函数就不能再次执行。

参考