单例模式是一种创建型设计模式, 让你能够保证一个类只有一个实例, 并提供一个访问该实例的全局节点。

使用场景

简单地过一下单例模式的场景,用几个例子来帮助我们理解:

  1. 资源共享:假设一个多线程的 web 服务,每当有客户端连接时,都需要访问配置管理器,来获取配置中定义的数据库连接参数、日志记录级别等。我们知道配置信息的加载和解析可能会消耗一定的资源和时间,如果每次有新连接时都创建新的配置管理器实例,极大可能会导致资源过度消耗。这时使用单例模式,我们可以确保只有一个配置管理器实例存在,所有线程都共享同一个配置管理器实例,从而避免资源浪费、重复创建对象的性能问题。
  2. 控制对象的创建:比如使用日志记录器时,我们希望在整个应用程序中使用同一个日志记录器实例来记录日志信息。通过使用单例模式,我们可以限制日志记录器类的实例化次数为1,确保所有的地方都使用这一个日志记录器实例,这样就可以使日志的管理更加统一和一致。
  3. 全局访问点:想象一个游戏中的全局数据管理器,负责管理玩家数据、游戏状态等信息。通过使用单例模式,我们可以在任何需要访问全局数据的地方都通过统一的访问点来获取数据管理器实例,这样可以简化代码,提高代码的可维护性和扩展性。

代码实现

单例模式的实现有两种不同的思想:

  • 饿汉式
  • 懒汉式

饿汉式

饿汉式单例模式的设计思路:先创建,“饿了”的就能马上“吃”。
代码实现其实很简单,初始化时创建结构体,然后提供一个全局访问方法即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package singleton

type singleton struct{}

var singleInstance *singleton

// init 方法会在程序初始化时自动执行,而且只会执行一次
func init() {
// 刚好能让我们创建结构体
singleInstance = &singleton{}
}

// GetInstance 获取实例
func GetInstance() *singleton {
return singleInstance
}

懒汉式

懒汉式单例模式的设计思路:用的时候再来创建。
代码实现,在 go 中有两种:

  • 双重检查锁定
  • sync.Once(go 特色)

双重检查锁定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package singleton

import (
"fmt"
"sync"
)

var lock = &sync.Mutex{}

type single struct {}

var singleInstance *single

// 用时创建实例(双重检查锁)
func GetInstance() *single {
// 第一重检查,为了已有实例时快速返回,避免操作锁的耗时
if singleInstance == nil {
// 这一步,还没加锁,所以高并发的时候可能会有多个协程进来
lock.Lock()
defer lock.Unlock()
// 第二重检查,为了确保即使有多个协程绕过了第一重检查, 也只能有一个可以创建单例实例。
if singleInstance == nil {
singleInstance = &single{}
}
}

return singleInstance
}

sync.Once

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package singleton

import (
"fmt"
"sync"
)

type single struct {
}

var (
singleInstance *single
once sync.Once
)

// 用时创建实例(sync.Once)
func GetInstance() *single {
// Do 方法只能执行一次,所以保证了单例的正确性
once.Do(func() {
singleInstance = &single{}
})
return singleInstance
}

结语

单例模式是我们日常开发中,较为常用的设计模式。实现起来很简单,但是也有一些小细节需要注意。


本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。