Go Once
一、介绍
Once 常常用来初始化单例资源 ,或者并发访问只需初始化一次的共享资源,或者在测试的时候初始化一次测试资源。
二、结构体
go
type Once struct {
_ noCopy
// done indicates whether the action has been performed.
// It is first in the struct because it is used in the hot path.
// The hot path is inlined at every call site.
// Placing done first allows more compact instructions on some architectures (amd64/386),
// and fewer instructions (to calculate offset) on other architectures.
done atomic.Bool
m Mutex
}
- noCopy : 编译期检查,避免值拷贝导致的状态失效,无内存开销
- done : 标记函数是否已执行(核心状态)
atomic.Bool类型(原子布尔型 ): 提供了原子的Load()/Store()方法,保证并发读写的安全性,且无需加锁 - m : 保证并发下仅执行一次的
互斥锁
三、核心方法
3.1 Do(f func())
go
func (o *Once) Do(f func()) {
// Note: Here is an incorrect implementation of Do:
//
// if o.done.CompareAndSwap(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 o.done.Store must be delayed until after f returns.
// 快速路径:原子读取 done,无锁,开销极小
if !o.done.Load() {
// Outlined slow-path to allow inlining of the fast-path.
// 慢路径:加锁,保证只有一个 goroutine 执行 f
o.doSlow(f)
}
}
func (o *Once) doSlow(f func()) {
// 加锁
o.m.Lock()
defer o.m.Unlock()
// 双重检查:避免加锁期间其他 goroutine 已执行完 f
if !o.done.Load() {
// 执行完 f 后,原子设置 done 为 true
defer o.done.Store(true)
f()
}
}

逻辑:
- 所有 goroutine 先读取
done(原子操作,无锁),若为true直接返回 - 若 done 为
false,竞争m锁,只有一个 goroutine 能拿到 - 拿到锁的
goroutine再次检查done(防止等待锁期间其他 goroutine 已执行),执行f后将done置为true; - 其他等待锁的 goroutine 拿到锁后,发现
done=true,直接释放锁返回
四、注意事项
1. 禁止值拷贝 Once 实例
sync.Once 是有状态的结构体 (内部维护 done 和 m),值拷贝会生成全新的实例,导致 "仅执行一次" 的语义失效
2. Do(f) 中 f 发生 panic 也会标记为已执行
Once 的设计逻辑是:只要进入 f() 的执行流程(哪怕 panic),done 就会被置为 true,后续调用不会再执行 f。
go
package main
import (
"fmt"
"sync"
)
var once sync.Once
func riskyFunc() {
panic("初始化函数panic了") // 执行panic
}
func main() {
// 第一次调用:执行riskyFunc,触发panic,但done会被置为true
defer func() {
if err := recover(); err != nil {
fmt.Println("捕获panic:", err)
}
}()
once.Do(riskyFunc)
// 第二次调用:done已为true,不会执行riskyFunc
fmt.Println("第二次调用Do:")
once.Do(riskyFunc)
fmt.Println("执行完成")
}
3. 一个 Once 实例只能绑定一个函数,不能复用
Once 是 "执行一次操作" 的语义,而非 "对每个函数执行一次"。一个 Once 实例调用 Do(f1) 后,再调用 Do(f2),f2 永远不会执行。
go
package main
import (
"fmt"
"sync"
)
var once sync.Once
func f1() { fmt.Println("执行f1") }
func f2() { fmt.Println("执行f2") }
func main() {
once.Do(f1) // f1执行,done置为true
once.Do(f2) // done已为true,f2永远不执行
}
不同的 "一次性操作" 对应不同的 Once 实例
4. f 函数不能有参数 / 返回值(需用闭包适配)
Once.Do(f) 的参数要求是 func()(无参数、无返回值),如果需要传递参数或接收返回值,必须通过闭包实现
go
package main
import (
"fmt"
"sync"
)
var once sync.Once
var result int
// 带参数的函数
func calc(a, b int) int {
return a + b
}
func main() {
once.Do(func() {
// 闭包内调用带参数的函数,并接收返回值
result = calc(10, 20)
})
fmt.Println("计算结果:", result) // 输出 30
}
5. 避免 Once 依赖循环或初始化顺序问题
如果 f() 内部又调用了同一个 Once 的 Do(),会导致死锁(因为 Once 的锁是不可重入的)
go
package main
import (
"fmt"
"sync"
)
var once sync.Once
func f() {
once.Do(g) // 内部再次调用同一个Once的Do,导致死锁
}
func g() { fmt.Println("执行g") }
func main() {
once.Do(f)
}
五、使用
- Once 适合 "延迟初始化 ",而非 "懒加载所有资源 "
Once 的性能很高,但如果把所有初始化逻辑都塞到一个 f() 里,会导致首次调用的延迟过高。建议按模块拆分 Once 实例,按需初始化 - 不要手动修改 Once 的内部字段
Once 的 done 和 m 是未导出字段,手动通过反射修改会破坏其并发安全性,属于典型的 "黑客行为",绝对禁止。 - Once 与 init() 函数的区别
init():程序启动时执行(包级),无法控制执行时机,且一定会执行Once.Do(f):首次调用时执行,可控制时机,且仅执行一次;
场景选择:如果是包级的基础初始化,用 init();如果是按需初始化(比如首次使用数据库连接时),用 Once