有使用过 Once 吗?
有使用过 Once 吗?
提示
题眼: 单例,只执行一次。
sync.Once 用于确保某段逻辑只执行一次。无论多少个 goroutine 同时调用 Do,传入的函数最多只会真正执行一遍。
它的底层实现很经典:一个标志位加一把互斥锁。
type Once struct {
done uint32 // 标记是否已执行
m Mutex // 互斥锁,用于保护临界区
}
func (o *Once) Do(f func()) {
// 快速检查,避免每次都加锁
if atomic.LoadUint32(&o.done) == 0 {
o.doSlow(f)
}
}
func (o *Once) doSlow(f func()) {
o.m.Lock()
defer o.m.Unlock()
// 双重检查
if o.done == 0 {
defer atomic.StoreUint32(&o.done, 1)
f()
}
}它在实现上有两个常见优化点:
- 先用原子操作读取
done,减少不必要的加锁。 - 加锁后再做一次双重检查,避免多个 goroutine 同时进入慢路径。
需要注意的是:
Once不适合嵌套调用自身相关逻辑,否则可能死锁。- 传入
Do的函数即使执行报错或触发panic,这一轮也算执行过,后面不会自动重试。
重要
参考答案:
Once 是 Go 提供的一个非常实用的小工具,用来保证一个函数在并发场景下只执行一次。它特别适合做单例初始化、配置加载、连接池初始化这类操作。
它的实现思路很简单,一个 done 标志位配合一把互斥锁。先通过原子操作判断是否已经执行过,没执行过再进入加锁路径;真正执行前再检查一次,这就是典型的双重检查。
它的优点是简单、线程安全、性能也不错,因为绝大多数调用在第一次之后都只需要读一个原子标志位。缺点也很明显,就是它只认“是否执行过”,不关心执行是否成功。
所以实际开发里,如果 Once 里的初始化失败了,往往不能指望后续自动重试,通常要么在初始化失败时直接中止程序,要么自己额外设计重试和状态管理逻辑。
