MST

星途 面试题库

面试题:Go 中 sync.Once 实现单例模式的原理

请详细阐述 Go 语言中使用 sync.Once 实现单例模式的原理,包括 sync.Once 结构体的关键字段及其作用,以及在多 goroutine 环境下如何保证单例的唯一性。
28.5万 热度难度
编程语言Go

知识考点

AI 面试

面试题答案

一键面试

sync.Once 实现单例模式原理

  1. sync.Once 结构体
    • 在 Go 语言中,sync.Once 结构体定义如下:
    type Once struct {
        done uint32
        m    Mutex
    }
    
    • done 字段
      • 类型为 uint32,用于标记初始化是否已经完成。当 done 为 0 时,表示尚未初始化;当 done 为非 0 值时,表示已经完成初始化。这个字段使用原子操作来保证在多 goroutine 环境下的正确读写。
    • m 字段
      • 类型为 Mutex,即互斥锁。它的作用是在初始化操作时进行同步,避免多个 goroutine 同时执行初始化代码。
  2. 初始化过程
    • sync.Once 主要通过 Do 方法来实现单例初始化,Do 方法的定义如下:
    func (o *Once) Do(f func()) {
        if atomic.LoadUint32(&o.done) == 0 {
            o.doSlow(f)
        }
    }
    
    • 首先通过 atomic.LoadUint32 原子操作检查 done 字段,如果 done 已经非 0,说明初始化已经完成,直接返回,不再执行初始化函数 f
    • 如果 done 为 0,调用 doSlow 方法:
    func (o *Once) doSlow(f func()) {
        o.m.Lock()
        defer o.m.Unlock()
        if o.done == 0 {
            defer atomic.StoreUint32(&o.done, 1)
            f()
        }
    }
    
    • doSlow 方法中,首先获取互斥锁 m,这样可以保证同一时间只有一个 goroutine 能进入后续的初始化检查和执行。
    • 再次检查 done 字段(双重检查锁定机制),这是因为在获取锁之前可能有其他 goroutine 已经完成了初始化。
    • 如果 done 仍为 0,执行初始化函数 f,并在初始化完成后通过 atomic.StoreUint32 原子操作将 done 设置为 1,表示初始化完成。
  3. 多 goroutine 环境下单例唯一性保证
    • 原子操作done 字段使用原子操作 atomic.LoadUint32atomic.StoreUint32 来读取和设置,这样可以保证在多 goroutine 并发环境下,对 done 字段的读写操作是线程安全的,不会出现数据竞争导致的错误判断。
    • 互斥锁:在初始化过程中,通过互斥锁 m 来保证同一时间只有一个 goroutine 能够执行初始化函数 f。即使多个 goroutine 同时进入 Do 方法且都发现 done 为 0,由于互斥锁的存在,只有一个 goroutine 能真正执行 f,其他 goroutine 在获取锁时会被阻塞,待初始化完成后,其他 goroutine 再次检查 done 字段就会发现已经初始化完成,从而直接返回,确保了单例的唯一性。