Once 可以用来执行且仅仅执行一次动作, 常常被利用于单个对象的初始化场景.如单例模式
Go 实现单例模式有下面以下方法:
可以定义包(package) 级别的单例变量, 例如:
package abc
import time
var startTime = time.Now() // 包级别的单例变量
可以在 init 函数进行单例变量的初始化
package abc
var startTime time.Time
func init() {
startTime = time.Now() // 在初始化函数中初始化单例变量
}
在 main 函数开始执行的时候, 执行一个初始化函数:
package abc
var startTime time.Time
func initApp() {
startTime = time.Now() // 用户自定义的初始化函数
}
func main() {
initApp()
}
这三种方法都是线程安全, 并且后两种方法还可以根据传入的参数实现定制化的初始化操作. 但很多时候是要延迟进行初始化的. 所以对单例资源的初始化会使用下面的方法:
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")
}
}
这种方法比较简单, 但是比较浪费资源. 每次请求都需要竞争锁才能读取到这个连接, 而且连接创建好后不需要锁的保护了.
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 一样的同步原语很简单, 只需要使用一个 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()
}