Golang 单例模式: 揭秘五种实现方式

内容纲要

概述

单例是一种常用的模式。单例就是这个实例在应用的整个声明周期只有一个实例。单例在实现时根据初始化方式可以分为两类:

  1. 懒汉
  2. 饿汉

懒汉模式就是懒加载,在获取实例时才创建;饿汉就是程序启动时就创建实例。

个人建议能使用饿汉就使用饿汉,因为实例有什么问题可以提前发现和解决,别等到运行了一段时间发现实例有问题。但是实在不能用就用懒汉嘛,可以进行各种测试保证。

接下来,我将带大家探索在 Golang 中实现单例模式的五种方式。

一、懒汉模式 (简单实现)

在这个版本中,我们将创建一个实例,并在全局公开这个实例。当我们第一次试图获取这个单例的时候, 它将为我们创建一个。

在我们再次尝试获取它时,它将返回第一次创建的实例。

在以下代码中,首次调用 GetLazyInstance 创建并返回一个新的 LazySingleton 实例,随后的调用将会返回首次创建的实例。

package main

import "fmt"

var lazyinstance *LazySingleton

type LazySingleton struct {
}

func GetLazyInstance() *LazySingleton {
    if lazyinstance == nil {
        lazyinstance = &LazySingleton{}
    }
    return lazyinstance
}

func main() {
    s1 := GetLazyInstance()
    s2 := GetLazyInstance()
    if s1 == s2 {
        fmt.Println("s1 == s2") // 输出: s1 == s2
    } else {
        fmt.Println("s1 != s2")
    }
}

然而这个版本的实现仅在单线程环境下工作,在高并发的环境下可能会创建多个 LazySingleton 实例,这就违反了单例模式的原则了。

二、懒汉模式的线程安全型实现

为了解决懒汉模式在高并发环境下的问题,我们可以通过添加一个互斥锁 mux 来保证并发情况下的实例唯一性。

以下是加入互斥锁的实现版本:

package main

import "sync"

type LockInstance struct {
    s string
}

var mux sync.Mutex
var lockinstance *LockInstance

func GetLockInstance() *LockInstance {
    if lockinstance == nil {
        mux.Lock()
        defer mux.Unlock()
        println("create singleton instance")
        if lockinstance == nil {
            lockinstance = &LockInstance{
                s: "lock",
            }
        }
    }

    return lockinstance
}

func main() {
    var wg sync.WaitGroup
    for i := 0; i < 100; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            instance := GetLockInstance()
            println(instance.s)
        }()
    }
    wg.Wait()
}

这个版本的实现已经是线程安全的,但多线程环境下性能会有点不太好,因为每次获取实例操作都要获取锁。
不了解sync.Mutex的同学可以参考我的文章:逐步学习Go-sync.Mutex(详解与实战)

我们是不是有更好的办法?这就是接下来要说的第三种实现方式。

三、懒汉模式(使用 sync.Once 惰性实现)

这是最为推荐的一种实现方式。通过使用 sync.Once 类型,我们可以保证懒加载并且线程安全。 once.Do() 保证其中的函数只会执行一次。

package main

import "sync"

type ConcurrentSingleton struct {
}

var once sync.Once
var concurrentinstance *ConcurrentSingleton

func GetConcurrentInstance() *ConcurrentSingleton {
    once.Do(func() {
        println("init only once")
        concurrentinstance = &ConcurrentSingleton{}
    })

    return concurrentinstance
}

func main() {
    var wg sync.WaitGroup
    for i := 0; i < 100; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            instance := GetConcurrentInstance()
            println(instance)
        }()
    }
    wg.Wait()
}

看!在所有并发下,只初始化一次,而且拥有很好的性能。

不了解sync.Once的同学可以参考我的文章: 逐步学习Go-sync.Once(只执行一次)Exactly Once

四、饿汉模式

在饿汉模式中,单例在程序开始的时候就完成了初始化。这种方式的优点是加载速度快,线程安全,因为它没有在运行时进行多余的判断,但是对应的缺点就是不管该类是否被使用,先进性初始化了。

package main

type Singleton struct {
    s string
}

var instance *Singleton

func init() {
    instance = &Singleton{s: "hello"}
}

func GetInstance() *Singleton {
    return instance
}

func main() {
    println(GetInstance().s)
}

五、饿汉模式 (另一种实现)

这是另一种饿汉模式的实现,这种实现更加简洁,并且仍然保持了只有一个单例实例的原则。

package main

import "fmt"

type EagerInstance struct {
}

var eagerInstance = &EagerInstance{}

func GetEagerInstance() *EagerInstance {
    return eagerInstance
}

func main() {
    s3 := GetEagerInstance()
    s4 := GetEagerInstance()
    if s3 == s4 {
        fmt.Println("s3 == s4")
    } else {
        fmt.Println("s3 != s4")
    }
}

总结

目前我只探索到这五种单例方式,如果你还有更好的想法和方式,欢迎讨论。

发表评论

您的电子邮箱地址不会被公开。 必填项已用 * 标注

滚动至顶部