面试题答案
一键面试设计思路
- 使用
unsafe
包:unsafe
包提供了对Go语言内存的底层操作,允许我们进行指针操作,这在实现单例模式时用于直接操作内存地址,以达到控制对象初始化的目的。 - 原子操作:为了避免竞态条件,使用
atomic
包中的原子操作函数。原子操作可以保证在多线程环境下对共享资源的操作是原子性的,即不会被其他线程打断。 - 延迟初始化:通过一个标志位来判断单例对象是否已经初始化,只有在未初始化时才进行初始化操作,确保单例对象在第一次使用时才初始化。
代码实现
package main
import (
"atomic"
"fmt"
"unsafe"
)
// Singleton结构体定义单例对象
type Singleton struct {
// 这里可以添加单例对象需要的属性
}
var (
instance unsafe.Pointer
initialized uint32
)
// GetInstance获取单例实例
func GetInstance() *Singleton {
if atomic.LoadUint32(&initialized) == 1 {
return (*Singleton)(instance)
}
// 这里使用atomic.CompareAndSwapUint32来确保只有一次初始化成功
if atomic.CompareAndSwapUint32(&initialized, 0, 1) {
// 使用unsafe包进行内存分配和初始化
alloc := unsafe_Alloc(unsafe.Sizeof(Singleton{}))
instance = alloc
(*Singleton)(instance) = Singleton{}
return (*Singleton)(instance)
}
// 如果初始化已经完成,再次等待并返回
for atomic.LoadUint32(&initialized) != 1 {
// 可以在这里使用runtime.Gosched()来让出CPU,避免空转
}
return (*Singleton)(instance)
}
测试代码
func main() {
// 创建多个goroutine并发获取单例实例
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
s1 := GetInstance()
s2 := GetInstance()
fmt.Println(s1 == s2)
}()
}
wg.Wait()
}
在上述代码中:
instance
是一个unsafe.Pointer
类型的变量,用于存储单例对象的地址。initialized
是一个uint32
类型的标志位,用于标记单例对象是否已经初始化。GetInstance
函数首先检查initialized
标志位,如果已经初始化则直接返回单例对象。如果未初始化,则使用atomic.CompareAndSwapUint32
尝试将initialized
从0设置为1,如果设置成功则进行单例对象的初始化。如果设置失败则等待直到初始化完成。- 在
main
函数中,通过多个goroutine并发调用GetInstance
函数来测试单例模式的线程安全性。