MST

星途 面试题库

面试题:Go语言sync.Once的底层实现与定制扩展

深入分析Go语言sync.Once的底层源码实现,包括其使用的原子操作和状态机原理。基于此,假设你需要对sync.Once进行定制扩展,使其支持在初始化失败时进行重试机制,你将如何设计和实现这个扩展功能?请详细描述设计思路、关键代码结构以及可能遇到的问题和解决方案。
18.1万 热度难度
编程语言Go

知识考点

AI 面试

面试题答案

一键面试

1. sync.Once底层源码实现分析

  1. 结构体定义

    type Once struct {
        done uint32
        m    Mutex
    }
    
    • done 是一个 uint32 类型的原子变量,用于表示初始化是否完成。值为0表示未初始化,非0表示已初始化。
    • m 是一个互斥锁,用于在并发环境下保护初始化操作。
  2. Do方法实现

    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()
        }
    }
    
    • 首先通过 atomic.LoadUint32 原子操作快速检查 done 是否为0,若不为0则说明已经初始化,直接返回。
    • done 为0,则进入 doSlow 方法。这里使用互斥锁 m 加锁,再次检查 done 是否为0(双重检查锁定),防止多个协程同时通过了快速检查。
    • 执行初始化函数 f 后,通过 atomic.StoreUint32done 设置为1,表示初始化完成。
  3. 状态机原理

    • 状态机有两个状态:未初始化(done == 0)和已初始化(done!= 0)。
    • 初始状态为未初始化。当调用 Do 方法时,若处于未初始化状态,会执行初始化函数并转换到已初始化状态。一旦进入已初始化状态,后续调用 Do 方法都不会再次执行初始化函数。

2. 支持初始化失败重试机制的设计思路

  1. 增加重试次数和错误处理
    • Once 结构体中新增字段,用于记录重试次数和保存初始化过程中产生的错误。
  2. 修改 Do 方法逻辑
    • 在初始化失败时,根据重试次数决定是否再次尝试初始化。

3. 关键代码结构

type RetryOnce struct {
    done    uint32
    m       Mutex
    retry   int
    maxRetry int
    err     error
}

func (ro *RetryOnce) Do(f func() error) error {
    if atomic.LoadUint32(&ro.done) == 0 {
        return ro.doSlow(f)
    }
    if ro.err!= nil {
        return ro.err
    }
    return nil
}

func (ro *RetryOnce) doSlow(f func() error) error {
    ro.m.Lock()
    defer ro.m.Unlock()
    for ro.retry < ro.maxRetry {
        if ro.done == 0 {
            err := f()
            if err == nil {
                atomic.StoreUint32(&ro.done, 1)
                return nil
            }
            ro.err = err
            ro.retry++
        }
    }
    return ro.err
}
  1. RetryOnce结构体
    • retry 记录已经重试的次数。
    • maxRetry 表示最大重试次数。
    • err 用于保存初始化过程中产生的错误。
  2. Do方法
    • 首先快速检查 done 状态,若已初始化且无错误则直接返回。若已初始化但有错误,则返回错误。
    • 若未初始化,则调用 doSlow 方法。
  3. doSlow方法
    • 使用 for 循环进行重试,每次调用初始化函数 f,若成功则设置 done 并返回 nil,若失败则记录错误并增加重试次数。当达到最大重试次数仍失败时,返回错误。

4. 可能遇到的问题和解决方案

  1. 死锁问题
    • 问题:如果初始化函数 f 内部再次调用 Do 方法,可能会导致死锁。
    • 解决方案:在设计时明确禁止初始化函数 f 内部调用 Do 方法,或者通过增加检测逻辑,当发现递归调用时直接返回错误。
  2. 资源浪费问题
    • 问题:过多的重试可能导致资源浪费,例如网络请求重试过多会占用大量网络资源。
    • 解决方案:合理设置 maxRetry 值,根据具体业务场景进行调优。可以结合指数退避算法,随着重试次数增加,适当延长重试间隔,减少资源消耗。