Go语言设计模式之单例模式

2023-12-16

单例模式的概念非常好理解,但凡接触过设计模式的同学,都能讲出一二来。

1.模式介绍

单例设计模式(Singleton Design Pattern):一个类只允许创建一个对象(或者实例),那这个类就是一个单例类,这种设计模式就叫作单例模式。
单例模式最常见的使用就是在程序启动时候创建加载基础组件:配置文件、实例日志组件、数据库组件、缓存组件等等(很多时候这种加载方式会用IOC容器来实现)。
这些全局唯一的组件实例在程序使用能够带来:提高系统效率(数据库连接池、线程池)、杜绝多线程资源访问冲突(日志处理)等好处。

2.模式demo

根据单例模式实现的方式,可分为:饿汉模式懒汉模式

饿汉模式:单例模式最常见实现的方式,在类加载的时候静态实例就已经加载好了。
懒汉模式:只在使用的时候进行加载,有延迟加载的效果。

2.1 饿汉模式实现

由于饿汉模式是在程序加载的时候就创建并实例化了,所以无需考虑多并发的情况。

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 main

import "fmt"

type Singleton struct {
// 在这里定义单例对象的属性
}

var instance *Singleton = createInstance()

func createInstance() *Singleton {
// 在这里创建并初始化单例对象
return &Singleton{
// 初始化单例对象的属性
}
}

func GetInstance() *Singleton {
return instance
}

func main() {
// 使用单例模式获取实例
singletonInstance := GetInstance()

// 使用单例实例
fmt.Println(singletonInstance)
}

2.2 懒汉模式实现

由于懒汉模式是在使用的时候再创建实例,属于懒加载;此时程序已经启动并正在运行,此时创建实例可能会出现多线程的情况,所以要考虑并发问题。

2.2.1 普通实现方式

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
29
30
31
32
33
34
package main

import (
"fmt"
"sync"
)

type Singleton struct {
// 在这里定义单例对象的属性
}

var instance *Singleton
var mu sync.Mutex

func GetInstance() *Singleton {
if instance == nil {
mu.Lock()
defer mu.Unlock()
if instance == nil {
instance = &Singleton{
// 初始化单例对象的属性
}
}
}
return instance
}

func main() {
// 使用单例模式获取实例
singletonInstance := GetInstance()

// 使用单例实例
fmt.Println(singletonInstance)
}

2.2.2 Go语言特定实现方式

以上都是一般语言通用的实现方式,还有一种Go语言独有实现的方式——使用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
24
25
26
27
28
29
30
package main

import (
"fmt"
"sync"
)

type Singleton struct {
// 在这里定义单例对象的属性
}

var instance *Singleton
var once sync.Once

func GetInstance() *Singleton {
once.Do(func() {
instance = &Singleton{
// 初始化单例对象的属性
}
})
return instance
}

func main() {
// 使用单例模式获取实例
singletonInstance := GetInstance()

// 使用单例实例
fmt.Println(singletonInstance)
}

是不是很神奇?为什么不用判断instance是否存在?为什么不用加锁?

这一切在源码面前都没有秘密:

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
29
30
31
32
33
package sync

import (
"sync/atomic"
)


type Once struct {
done uint32 // 记录实例数量
m Mutex // 锁
}


func (o *Once) Do(f func()) {

// 原子操作判断实例数量是否0,
// 如果是0就创建新的实例
if atomic.LoadUint32(&o.done) == 0 {
o.doSlow(f)
}
}

func (o *Once) doSlow(f func()) {
// 加锁
o.m.Lock()
defer o.m.Unlock()
if o.done == 0 {
// 操作完传参的函数之后,
// 原子操作将实例变成1
defer atomic.StoreUint32(&o.done, 1)
f()
}
}

实际上就是Go语言在基础库层面帮我们封装了通用的单例模式实现方式。

3.源码解析

3.1 beego的orm链接实例

在beego框架中,orm组件就使用了单例模式。

beego会将数据库的配置信息通过orm.RegisterDataBase("default", "mysql", "root:123456@tcp(127.0.0.1:3306)/beego?charset=utf8")函数存储至beego的orm结构体orm.alias

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type alias struct {
Name string
Driver DriverType
DriverName string
DataSource string
MaxIdleConns int
MaxOpenConns int
ConnMaxLifetime time.Duration
StmtCacheSize int
DB *DB
DbBaser dbBaser
TZ *time.Location
Engine string
}

期间还生成数据库连接所使用的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
29
30
31
32
33
34
35
36
37
38
```Go
type DB struct {
// Atomic access only. At top of struct to prevent mis-alignment
// on 32-bit platforms. Of type time.Duration.
waitDuration int64 // Total time waited for new connections.

connector driver.Connector
// numClosed is an atomic counter which represents a total number of
// closed connections. Stmt.openStmt checks it before cleaning closed
// connections in Stmt.css.
numClosed uint64

mu sync.Mutex // protects following fields
freeConn []*driverConn // free connections ordered by returnedAt oldest to newest
connRequests map[uint64]chan connRequest
nextRequest uint64 // Next key to use in connRequests.
numOpen int // number of opened and pending open connections
// Used to signal the need for new connections
// a goroutine running connectionOpener() reads on this chan and
// maybeOpenNewConnections sends on the chan (one send per needed connection)
// It is closed during db.Close(). The close tells the connectionOpener
// goroutine to exit.
openerCh chan struct{}
closed bool
dep map[finalCloser]depSet
lastPut map[*driverConn]string // stacktrace of last conn's put; debug only
maxIdleCount int // zero means defaultMaxIdleConns; negative means 0
maxOpen int // <= 0 means unlimited
maxLifetime time.Duration // maximum amount of time a connection may be reused
maxIdleTime time.Duration // maximum amount of time a connection may be idle before being closed
cleanerCh chan struct{}
waitCount int64 // Total number of connections waited for.
maxIdleClosed int64 // Total number of connections closed due to idle count.
maxIdleTimeClosed int64 // Total number of connections closed due to idle time.
maxLifetimeClosed int64 // Total number of connections closed due to max connection lifetime limit.

stop func() // stop cancels the connection opener.
}

并将sql.DB实例以属性的方式与orm.alias关联起来。
实例化如下的结构体:

image

以上结构体在每次添加(使用addAliasWthDB函数)的时候会根据“别名alias”创建唯一的实例。
具体的调用函数为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func addAliasWthDB(aliasName, driverName string, db *sql.DB, params ...DBOption) (*alias, error) {
existErr := fmt.Errorf("DataBase alias name `%s` already registered, cannot reuse", aliasName)
// 根据hash表判断是否存在,如果存在,则直接返回
if _, ok := dataBaseCache.get(aliasName); ok {
return nil, existErr
}

// 如果hash表中不存在,则创建新的实例对象。
al, err := newAliasWithDb(aliasName, driverName, db, params...)
if err != nil {
return nil, err
}

if !dataBaseCache.add(aliasName, al) {
return nil, existErr
}

return al, nil
}

以上函数中使用一张hash表判断别名alias为xxx的实例存不存在,这个hash表就在orm包中内部结构体_dbCachecache属性中

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
// database alias cacher.
type _dbCache struct {
mux sync.RWMutex
// 判断alias是否存在所使用的hash表
cache map[string]*alias
}

// 查询hash表中是否存在实例
func (ac *_dbCache) get(name string) (al *alias, ok bool) {
ac.mux.RLock()
defer ac.mux.RUnlock()
al, ok = ac.cache[name]
return
}

// 向hash表中存入
func (ac *_dbCache) add(name string, al *alias) (added bool) {
ac.mux.Lock()
defer ac.mux.Unlock()
if _, ok := ac.cache[name]; !ok {
ac.cache[name] = al
added = true
}
return
}

beego的orm注册的流程图如下:
image
可见,beego中相同alias的orm实例都是相同的,每次获取实例都是同一个实例,虽然代码书写上与传统的单例模式有些差别,但是本质上还是使用了单例这种设计模式。

请关注我

微信公众号:搜索 码农RyuGou

或者扫码