once
阅读提示
- 适用版本:Go 1.21+(示例在 Go 1.22 下验证)· 前置知识:sync.Mutex · 阅读时长:约 8 分钟
sync.Once 是 Go 标准库中的一个同步工具,用于确保某个函数在并发环境下只执行一次。它通常用于延迟初始化、全局资源的初始化等场景,确保某个特定的操作只会被执行一次,即使有多个协程并发执行。
go
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
var once sync.Once
for i := range 10 {
wg.Add(1)
go func() {
defer wg.Done()
once.Do(func() {
fmt.Println(i)
})
}()
}
wg.Wait()
}在上面的代码中,总共有 10 个协程,但无论如何,只有一个协程会执行 once.Do() 中的 fmt.Println(i)。具体是哪一个协程会打印,取决于哪个协程最先到达 once.Do()。
结构
go
type Once struct {
done atomic.Uint32
m Mutex
}它的内部结构非常简单
done,一个原子值,用于表示是否被执行过m,互斥锁,用于阻塞其它想要执行的协程
它的原理就是在执行前先加锁,然后更新done,执行完毕后解锁。
Do
go
func (o *Once) Do(f func()) {
if o.done.Load() == 0 {
o.doSlow(f)
}
}
func (o *Once) doSlow(f func()) {
o.m.Lock()
defer o.m.Unlock()
if o.done.Load() == 0 {
defer o.done.Store(1)
f()
}
}代码整体上非常简单,流程如下:
- 直接加载原子值,如果已经执行过了就直接返回
- 没有执行过,则尝试持有锁,这里可能是多个协程在竞争互斥锁
- 当胜者成功持有锁后,还需要再一次判断
done的值,因为在你持有锁后,别人可能已经完成加锁-执行-解锁一条龙了,这时你才刚被唤醒。 - 执行目标函数
- 更新
done值 - 释放锁
小结
sync.Once 是一个非常简洁而高效的同步工具,它确保了并发环境下某个操作只执行一次,从而避免了重复工作或资源的浪费。
完整示例
下面的程序可以直接运行:复制到文件后执行 go run main.go,或粘贴到 Go Playground 运行。
go
package main
import (
"fmt"
"sync"
)
func main() {
var once sync.Once
var wg sync.WaitGroup
// 五个协程竞争初始化,实际只有一个会执行
for i := 0; i < 5; i++ {
wg.Add(1)
go func() {
defer wg.Done()
once.Do(func() { fmt.Println("init once") })
}()
}
wg.Wait()
// 之后的调用都是空操作
once.Do(func() { fmt.Println("this will not print") })
fmt.Println("done")
}预期输出:
text
init once
done常见错误与踩坑
Once保护的是"一个函数",不是"一处调用"。 多处初始化应各自使用独立的Once。Do内部 panic 仍会标记为已完成。 后续调用不会重试;初始化逻辑要保证健壮。- 使用过的
Once不能拷贝。 它内部包含互斥锁,拷贝会破坏"只执行一次"的保证。 - 不要把廉价操作包进
Do。 虽然快路径是原子操作,但闭包与加载仍有成本。 - Go 1.21+ 提供封装好的助手。
sync.OnceFunc/sync.OnceValue可直接使用。
更新记录
- 2026-10-14:新增"阅读提示"、完整可运行示例(含预期输出)、常见错误与踩坑清单,以及本更新记录。
