MST

星途 面试题库

面试题:Go语言中sync.Once如何保证只执行一次初始化

请详细阐述Go语言里sync.Once的原理,以及在代码中如何正确使用它来确保某段初始化代码只执行一次。并举例说明在并发场景下sync.Once可能会面临的问题及解决方法。
44.6万 热度难度
编程语言Go

知识考点

AI 面试

面试题答案

一键面试

sync.Once原理

  1. 数据结构sync.Once内部包含一个done标志位和一个m互斥锁。其结构体定义类似如下:
type Once struct {
    done uint32
    m    Mutex
}
  1. 初始化逻辑done标志位初始值为0,代表尚未执行初始化操作。done使用uint32类型主要是为了可以使用原子操作。
  2. Do方法Once.Do(f)方法用于执行初始化函数f。在调用Do方法时,首先会使用原子操作检查done标志位。如果done为0,说明初始化操作还未执行,此时会锁定互斥锁m,再次检查done(防止在获取锁之前其他协程已经完成初始化),若done仍为0,则执行传入的函数f,执行完毕后将done置为1,最后解锁互斥锁。如果done已经为1,说明初始化操作已经执行过,直接返回,不再执行f

正确使用方式

  1. 单例模式示例
package main

import (
    "fmt"
    "sync"
)

type Singleton struct {
    data string
}

var instance *Singleton
var once sync.Once

func GetInstance() *Singleton {
    once.Do(func() {
        instance = &Singleton{
            data: "initial data",
        }
    })
    return instance
}

在上述代码中,GetInstance函数使用sync.Once来确保instance只被初始化一次。无论有多少个协程同时调用GetInstanceinstance只会被创建一次。

并发场景下可能面临的问题及解决方法

  1. 问题:如果Once.Do方法中执行的初始化函数f发生恐慌(panic),done标志位仍会被设置为1,后续再次调用Do方法时不会重新执行f。这可能导致程序处于错误状态,因为初始化未正确完成。
  2. 解决方法:在初始化函数f内部进行适当的错误处理,避免恐慌。如果无法避免恐慌,可以通过封装Once.Do调用,在外部捕获恐慌并进行相应处理。例如:
func SafeDo(once *sync.Once, f func()) (err error) {
    defer func() {
        if r := recover(); r != nil {
            err = fmt.Errorf("panic in Once.Do: %v", r)
        }
    }()
    once.Do(f)
    return nil
}

然后在使用时:

func main() {
    var once sync.Once
    err := SafeDo(&once, func() {
        // 可能发生恐慌的初始化操作
        panic("simulated panic")
    })
    if err != nil {
        fmt.Println("Error:", err)
    }
}

这样可以在Once.Do发生恐慌时,捕获并处理恐慌,避免done标志位被错误设置导致后续问题。