We have seen the decoration and option patterns in Golang's common design patterns before, and today we're going to look at the simplest singleton pattern in Golang's design patterns. The role of the singleton pattern is to ensure that no matter how many times the object is instantiated, only one instance exists globally. According to this feature, we can apply it to global unique configuration, database connection objects, file access objects, etc. There are many ways to implement the singleton pattern in the Go language. Let's take a look at it together.
Hungry Chinese
It is very simple to implement the singleton pattern in a hungry Chinese style, just look at the code directly:
package singleton
type singleton struct{}
var instance = &singleton{}
func GetSingleton() *singleton {
return instance
}
The singleton package will automatically initialize the instance instance when it is imported. When using it, the singleton object of the singleton structure can be obtained by calling the singleton.GetSingleton() function.
The singleton object in this way is created immediately when the package is loaded, so this way is called the hungry Chinese style. An alternative implementation is called lazy, in which the instance is created the first time it is used.
It should be noted that although the singleton pattern is simple to implement, it is not recommended in most cases. Because if there is too much initialization content when the singleton is instantiated, it will cause the program to load for a long time.
lazy
Next, let's take a look at how to implement the singleton pattern in a lazy style:
package singleton
type singleton struct{}
var instance *singleton
func GetSingleton() *singleton {
if instance == nil {
instance = &singleton{}
}
return instance
}
Compared to the Hungry-style implementation, the Lazy-style moves the code that instantiates the singleton structure into the GetSingleton() function. This delays the instantiation of the object until GetSingleton() is first called.
However, it is not very reliable to implement a singleton through the judgment of instance == nil. If there are multiple goroutines calling GetSingleton() at the same time, concurrency safety cannot be guaranteed.
Singletons that support concurrency
If you've written concurrent programming in Go, you should quickly be able to figure out how to solve the concurrency safety problem of the lazy singleton pattern, such as the following:
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
}
The modification of the above code is through the locking mechanism, that is, the following two lines of code are added at the beginning of the GetSingleton() function:
mu.Lock()
defer mu.Unlock()
The locking mechanism can effectively ensure that the function that implements the singleton pattern is concurrently safe.
However, the use of the lock mechanism also brings some problems, which makes the program perform locking and unlocking steps every time GetSingleton() is called, resulting in a decrease in program performance.
double lock
Locking will degrade program performance, but without locks, the concurrency safety of the program cannot be guaranteed. In order to solve this problem, someone proposed a double-check lock (Double-Check Locking) scheme:
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
}
As can be seen from the above, the so-called double locking is actually adding a layer of instance == nil judgment before the program is locked. In this way, both performance and security are taken into account. However, this makes the code look a bit strange. The outer layer has already judged instance == nil, but after locking, a second instance == nil judgment is made.
In fact, the judgment of instance == nil in the outer layer is to improve the execution efficiency of the program, avoid the original operation of locking every time GetSingleton() is called, and make the granularity of locking more refined. Simply put, if the instance already exists, there is no need to enter the if logic, and the program can directly return the instance. The inner layer's instance == nil judgment considers concurrency safety. Considering that in extreme cases, multiple goroutines reach the step of locking at the same time, the inner layer judgment will play a role here.
Gopher idiomatic scheme
Although the double locking mechanism takes into account performance and concurrency safety, the code is obviously ugly and does not meet the expectations of the majority of Gophers. Fortunately, the Go language provides the Once mechanism in the sync package to help us write more elegant code:
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 is a structure, and the atomic operation and locking mechanism are used to ensure concurrency safety inside the Do method, and once.Do can ensure that &singleton{} is only created once when multiple goroutines are executed at the same time.
In fact, Once is not mysterious. Its internal implementation is very similar to the double locking mechanism used above, except that instance == nil is replaced by atomic operation. Interested students can check the corresponding source code.
Summarize
The above are several common routines for implementing the singleton pattern in the Go language. After comparison, we can draw a conclusion that the most recommended way is to use once.Do to implement. The sync.Once package helps us hide some details, but it can make the code readable. Readability is greatly improved.
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。