Once 可以用来执行且仅仅执行一次动作, 常常被利用于单个对象的初始化场景.如单例模式

Go 实现单例模式有下面以下方法:

这三种方法都是线程安全, 并且后两种方法还可以根据传入的参数实现定制化的初始化操作. 但很多时候是要延迟进行初始化的. 所以对单例资源的初始化会使用下面的方法:

package main

import (
	"net"
	"sync"
	"time"
)

// 使用互斥锁保证 goroutine 安全
var connMu sync.Mutex
var conn net.Conn

func getConn() net.Conn {
	connMu.Lock() //使用锁
	defer connMu.Unlock()

	// 返回已创建好的连接
	if conn != nil {
		return conn
	}
	
	// 创建连接
	conn, _ = net.DialTimeout("tcp", "baidu.com:80", 10*time.Second)
	return conn
}

// 使用连接
func main() {
	conn := getConn()
	if conn == nil {
		panic("conn is nil")
	}
}

这种方法比较简单, 但是比较浪费资源. 每次请求都需要竞争锁才能读取到这个连接, 而且连接创建好后不需要锁的保护了.

Once 的使用方法

sync.Once 暴露一个方法 Do, 可以多次调用 Do 方法, 但是只有第一次调用 Do 方法时参数 f 才会执行, 这里 f 是一个无参数, 无返回值的函数

package main

import (
	"fmt"
	"sync"
)

func main() {
	var once sync.Once
	
	// 第一个初始化函数
	f1 := func() {
		fmt.Println("in f1")
	}
	once.Do(f1) // 打印出 "in f1"

	// 第二个初始化函数
	f2 := func() {
		fmt.Println("in f2")
	}
	once.Do(f2) // 无输出
}

因为这里的参数 f 是一个无参数, 无返回值的函数. 所以可能会通过闭包的方式引用外面的参数.如:

var addr = "baidu.com"

var conn net.Conn
var err error

once.Do(func() {
	conn, err = net.Dial("tcp", addr)
})

还有结合数据结构的, 如 math/big/sqrt.go 的 treeOnce

// 值是 3.0 或者 0.0 的一个数据结构
var threeOnce struct {
	sync.Once
	v *Float
}

// 返回此数据结构的值. 如果还没初始化为3.0, 则初始化
func three() *Float {
	threeOnce.Do(func() {
		threeOnce.v = NewFloat(3.0)
	})
	return threeOnce.v
}

这个数据结构将 sync.Once 和 *Float 封装一个对象, 提供了只初始化一次的值 v

总结: Oncce 常常用来初始化单例资源, 或者并发访问只需要初始化一次的共享的资源, 或者在测试时初始化一次的测试资源

Once 的实现

可能认为实现一个如 Once 一样的同步原语很简单, 只需要使用一个 flag 标记是否初始化过即可, 最多使用 atomic 原子操作这个 flag

type EasyOnce struct {
	done uint32
}

func (o *EasyOnce) Do(f func()) {
	// 虽然控制只有一个 goroutine 执行 f, 但是可能会导致一些 goroutine 以为初始化完成了
	if !atomic.ComareAndSwapUint32(&o.done, 0, 1) {
		return
	}
	f()
}