Singleton Pattern in Golang

English 简体中文 Tiếng Việt
Summary

The Singleton pattern ensures a single instance of an object globally, useful for shared resources like configurations or database connections. In Go, initial implementations like eager or basic lazy initialization present issues with either premature resource allocation or lack of concurrency safety. While a mutex can secure lazy initialization, it introduces performance overhead on every access, leading to more complex solutions like double-checked locking to mitigate this. For a robust and idiomatic approach, Go developers should leverage sync.Once, which elegantly guarantees a single, thread-safe initialization without explicit locking boilerplate, making it the recommended method.

单例模式是软件设计中最简单的设计模式。它确保无论对象被实例化多少次,都只有一个对象的实例存在于全局。基于单例模式的特性,它可以应用于全局唯一配置、数据库连接对象、文件访问对象等场景。在Go语言中,有多种实现单例模式的方法。今天,让我们一起学习其中的一些方法。

提前初始化

使用提前初始化实现单例模式非常简单。让我们直接看看代码:

package singleton
type singleton struct{}
var instance = &singleton{}
func GetSingleton() *singleton {
	return instance
}

当导入singleton包时,实例instance会自动初始化。要获取singleton结构体的单例对象,只需调用singleton.GetSingleton()函数。

由于单例对象在包加载期间立即创建,因此这种实现被称为提前初始化。与此对应的延迟初始化,只有在第一次使用时才创建实例。

需要注意的是,虽然提前初始化实现单例模式很简单,但通常不推荐,尤其是在单例实例的初始化过程很耗时的情况下。这会导致程序加载时间变长。

延迟初始化

接下来,让我们看看如何使用延迟初始化实现单例模式:

package singleton
type singleton struct{}
var instance *singleton
func GetSingleton() *singleton {
	if instance == nil {
		instance = &singleton{}
	}
	return instance
}

与提前初始化的实现相比,我们将实例化singleton结构体的代码移到了GetSingleton()函数内部。通过这样做,我们将对象的实例化延迟到第一次调用GetSingleton()时。

但是,使用条件instance == nil来实现单例并不完全可靠,因为它不能保证在多个goroutine同时调用GetSingleton()时的并发安全。

并发安全单例

如果你有在Go中进行并发编程的经验,你可以很快想到一个解决延迟初始化单例模式中并发安全问题的方案。

package singleton
import "sync"
type singleton struct{}
var instance *singleton
var mu sync.Mutex
func GetSingleton() *singleton {
	mu.Lock()
	defer mu.Unlock()
	if instance == nil {
		instance = &singleton{}
	}
	return instance
}

我们对代码的主要修改是在GetSingleton()函数的开头添加了以下两行:

mu.Lock()
defer mu.Unlock()

通过使用锁机制,我们可以确保这种单例模式的实现是并发安全的。
但是,这种方法有一个缺点。由于使用了锁,每次调用GetSingleton()时,程序都会经历获取和释放锁的过程。这可能会导致程序性能下降。

双重检查锁定

锁定会导致性能下降,但我们仍然需要使用锁来确保并发安全。因此,有人提出了双重检查锁定的解决方案:

package singleton
import "sync"
type singleton struct{}
var instance *singleton
var mu sync.Mutex
func GetSingleton() *singleton {
	if instance == nil {
		mu.Lock()
		defer mu.Unlock()
		if instance == nil {
			instance = &singleton{}
		}
	}
	return instance
}

正如你所看到的,所谓的双重检查锁定实际上是在获取锁之前添加了一个额外的instance == nil检查。这种方法结合了性能和安全方面的考虑。

然而,这段代码可能看起来有点奇怪。由于外部检查instance == nil已经执行过了,在获取锁之后再执行第二次instance == nil检查似乎是多余的。事实上,外部instance == nil检查是为了提高程序的执行效率。如果instance已经存在,则无需进入if逻辑,程序可以直接返回instance。这避免了每次GetSingleton()调用都获取锁的开销,使锁定更加细粒度。内部instance == nil检查是为了考虑并发安全。在极端情况下,当多个goroutine同时到达获取锁的点时,内部检查就会发挥作用。

Gopher的首选方法

虽然双重检查锁定机制允许我们同时实现性能和并发安全,但代码看起来有点丑陋,并不符合大多数Gopher的期望。幸运的是,Go语言提供了sync包中的Once机制,它允许我们编写更优雅的代码:

package singleton
import "sync"
type singleton struct{}
var instance *singleton
var once sync.Once
func GetSingleton() *singleton {
	once.Do(func() {
		instance = &singleton{}
	})
	return instance
}

Once是一个结构体,它通过使用原子操作和锁机制来确保Do方法内的并发安全。once.Do方法保证了&singleton{}的创建只发生一次,即使多个goroutine同时执行。

实际上,Once并不神秘。它的内部实现与前面提到的双重检查锁定机制非常相似,只是它用原子操作代替了instance == nil。感兴趣的读者可以参考相应的源代码了解更多细节。

总结

以上是在Go中实现单例模式的几种常见方法。比较之后,我们可以得出结论,最推荐的方法是使用once.Dosync.Once包有助于隐藏一些细节,同时显著提高代码可读性。

TUTORIAL GOLANG SINGLETON PATTERN

  RELATED

  COMMENTS

0

No comment for this article.