面试题答案
一键面试1. sync.Once底层源码实现分析
-
结构体定义
type Once struct { done uint32 m Mutex }
done
是一个uint32
类型的原子变量,用于表示初始化是否完成。值为0表示未初始化,非0表示已初始化。m
是一个互斥锁,用于在并发环境下保护初始化操作。
-
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.StoreUint32
将done
设置为1,表示初始化完成。
- 首先通过
-
状态机原理
- 状态机有两个状态:未初始化(
done == 0
)和已初始化(done!= 0
)。 - 初始状态为未初始化。当调用
Do
方法时,若处于未初始化状态,会执行初始化函数并转换到已初始化状态。一旦进入已初始化状态,后续调用Do
方法都不会再次执行初始化函数。
- 状态机有两个状态:未初始化(
2. 支持初始化失败重试机制的设计思路
- 增加重试次数和错误处理
- 在
Once
结构体中新增字段,用于记录重试次数和保存初始化过程中产生的错误。
- 在
- 修改
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
}
- RetryOnce结构体
retry
记录已经重试的次数。maxRetry
表示最大重试次数。err
用于保存初始化过程中产生的错误。
- Do方法
- 首先快速检查
done
状态,若已初始化且无错误则直接返回。若已初始化但有错误,则返回错误。 - 若未初始化,则调用
doSlow
方法。
- 首先快速检查
- doSlow方法
- 使用
for
循环进行重试,每次调用初始化函数f
,若成功则设置done
并返回 nil,若失败则记录错误并增加重试次数。当达到最大重试次数仍失败时,返回错误。
- 使用
4. 可能遇到的问题和解决方案
- 死锁问题
- 问题:如果初始化函数
f
内部再次调用Do
方法,可能会导致死锁。 - 解决方案:在设计时明确禁止初始化函数
f
内部调用Do
方法,或者通过增加检测逻辑,当发现递归调用时直接返回错误。
- 问题:如果初始化函数
- 资源浪费问题
- 问题:过多的重试可能导致资源浪费,例如网络请求重试过多会占用大量网络资源。
- 解决方案:合理设置
maxRetry
值,根据具体业务场景进行调优。可以结合指数退避算法,随着重试次数增加,适当延长重试间隔,减少资源消耗。