Go语言实现单例模式的3种方法

后端 潘老师 3个月前 (02-06) 69 ℃ (0) 扫码查看

Go语言如何实现单例模式?有哪几种方法?实现单例模式(Singleton Pattern)的核心使命是确保一个类仅有一个实例,并为程序提供一个全局访问该实例的入口。在实际场景中,当你需要严格控制某个对象的实例数量为一个时,单例模式就大显身手了,比如数据库连接池、日志管理器、配置管理器等组件的设计,它都能派上用场。

接下来,我们深入Go语言的世界,介绍3种实现单例模式的方式。

1. 线程安全的懒汉式单例

懒汉式单例的设计思路十分巧妙,它不会在程序启动时就迫不及待地创建实例,而是一直推迟到第一次被调用时才进行实例化操作。在多线程环境下,为了保证这种延迟创建的安全性,Go语言标准库中的 sync.Once 发挥了关键作用,它能确保实例仅被创建一次。

package main

import (
    "fmt"
    "sync"
)

var wg sync.WaitGroup

// Singleton 类型
type Singleton struct {
}

var instance *Singleton
var once sync.Once

// GetInstance 提供全局唯一的实例
func GetInstance() *Singleton {
    once.Do(func() {
        instance = &Singleton{}
    })
    return instance
}

func main() {
    // 获取单例实例
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(index int) {
            defer wg.Done()
            s1 := GetInstance()
            fmt.Printf("index %d, memery address: %pn", index, s1)
        }(i)
    }
    wg.Wait()
}

运行上述代码,你会得到类似这样的结果:

index 0, memery address: 0x56c480
index 5, memery address: 0x56c480
index 4, memery address: 0x56c480
index 2, memery address: 0x56c480
index 7, memery address: 0x56c480
index 9, memery address: 0x56c480
index 6, memery address: 0x56c480
index 8, memery address: 0x56c480
index 3, memery address: 0x56c480
index 1, memery address: 0x56c480

这表明,无论在多少个并发线程中获取实例,得到的都是同一个对象。

这里的 sync.Once 是Go语言并发编程中的一个重要同步原语,它保证了传入的函数只会被执行一次。而 once.Do 方法则是具体执行这个确保唯一性操作的关键,非常适合用于懒加载单例实例的场景。

2. 双重检查锁定(DCL)

双重检查锁定(Double-Checked Locking,简称DCL)是一种优化手段,旨在减少加锁带来的性能开销。它的实现方式是通过两次检查实例是否为空,从而提高获取单例的效率。

package main

import (
    "fmt"
    "sync"
)

var wg sync.WaitGroup

// Singleton 类型
type Singleton struct {
}

var instance *Singleton
var lock sync.Mutex

func GetInstance() *Singleton {
    if instance == nil {
        lock.Lock()
        defer lock.Unlock()
        if instance == nil {
            instance = &Singleton{}
        }
    }
    return instance
}

func main() {
    // 获取单例实例
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(index int) {
            defer wg.Done()
            s1 := GetInstance()
            fmt.Printf("index %d, memery address: %pn", index, s1)
        }(i)
    }
    wg.Wait()
}

在这个实现中,首先会进行一次快速检查,判断实例是否已经存在。如果实例为空,才会进入加锁流程。加锁后,会再次检查实例是否为空,只有在此时实例仍然为空的情况下,才会创建实例。这种方式避免了每次获取实例时都进行加锁操作,大大提升了性能。

3. 原子操作法

Go语言的 sync/atomic 包提供了强大的原子操作功能,利用这一特性,我们也可以实现线程安全的单例模式。

package main

import (
    "fmt"
    "sync"
    "sync/atomic"
    "unsafe"
)

var wg sync.WaitGroup

type Singleton struct {
}

var instance unsafe.Pointer

func GetInstance() *Singleton {
    // 使用原子操作获取实例
    if atomic.LoadPointer(&instance) == nil {
        newInstance := &Singleton{}
        atomic.StorePointer(&instance, unsafe.Pointer(newInstance))
    }
    return (*Singleton)(atomic.LoadPointer(&instance))
}

func main() {
    // 获取单例实例
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(index int) {
            defer wg.Done()
            s1 := GetInstance()
            fmt.Printf("index %d, memery address: %pn", index, s1)
        }(i)
    }
    wg.Wait()
}

在这段代码中,unsafe.Pointer 绕过了Go语言的类型系统,让我们可以直接操作内存地址。而 atomic.LoadPointeratomic.StorePointer 这两个原子操作函数,则确保了对指针的加载和存储操作是线程安全的,从而保证了单例实例赋值的安全性。

总结

在Go语言中,实现单例模式的方法多种多样,上述介绍的使用 sync.Once、双重检查锁定以及原子操作法是比较常见的方式。每种方法都有其独特的优缺点,在实际应用中,我们需要根据具体的业务场景和性能需求,选择最合适的实现方式,在保障线程安全的同时,实现性能的优化。


版权声明:本站文章,如无说明,均为本站原创,转载请注明文章来源。如有侵权,请联系博主删除。
本文链接:https://www.panziye.com/back/13293.html
喜欢 (0)
请潘老师喝杯Coffee吧!】
分享 (0)
用户头像
发表我的评论
取消评论
表情 贴图 签到 代码

Hi,您需要填写昵称和邮箱!

  • 昵称【必填】
  • 邮箱【必填】
  • 网址【可选】