Go语言设计模式(1)单例模式

Go语言设计模式(1)单例模式

单例模式的定义

个人认为单例模式是23种设计模式中最简单也最好理解的一种,定义如下:

Ensure a class has only one instance, and provide a global pointof access to it.

确保一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。

——《设计模式之禅》

那它有什么用呢?我目前在项目中遇到的最多的需要使用单例模式情况就是工具类——工具类一般都没有必要用一次就新建一个实例,所以使用单例模式来实现是非常合适的,当然到目前为止我只在Java中遇到过这个场景(毕竟Kotlin有语法层面的支持(object),Golang则很少需要这么做)。还有就是如果创建一个实例需要很大的资源开销(比如建立数据库连接等),那么也可以考虑使用单例模式。

单例模式的简单例子

我们使用Go语言重写《设计模式之禅》使用的臣子和皇帝的例子:

singleton/emperor.go

<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="go" cid="n19" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--font-monospace); font-size: 0.85rem; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-size: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248) !important; position: relative !important; width: inherit; border: 1px solid rgb(244, 244, 244); -webkit-font-smoothing: initial; line-height: 1.43rem; border-top-left-radius: 2px; border-top-right-radius: 2px; border-bottom-right-radius: 2px; border-bottom-left-radius: 2px; word-wrap: normal; margin: 0.8rem 0px !important; padding: 0.3rem 0px !important; background-position: inherit; background-repeat: inherit;">package singleton

import "fmt"

var instance *emperor // 实例

// emperor 皇帝结构体
// 这里不导出是因为如果导出(首字母大写),那么在其他包里就可以用e := &Emperor{}来创建新的实例了
// 那样就不是单例模式了,我们写这么一堆东西也就没有意义了
type emperor struct {
}

func init() {
// 初始化一个皇帝
instance = &emperor{}
}

// GetInstance 得到实例
func GetInstance() *emperor {
return instance
}

// Say 皇帝发话了
func (e *emperor) Say() {
fmt.Println("我就是皇帝某某某...")
}
</pre>

臣子类改写成了单元测试:

singleton/emperor_test.go

<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="go" cid="n27" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--font-monospace); font-size: 0.85rem; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-size: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248) !important; position: relative !important; width: inherit; border: 1px solid rgb(244, 244, 244); -webkit-font-smoothing: initial; line-height: 1.43rem; border-top-left-radius: 2px; border-top-right-radius: 2px; border-bottom-right-radius: 2px; border-bottom-left-radius: 2px; word-wrap: normal; margin: 0.8rem 0px !important; padding: 0.3rem 0px !important; background-position: inherit; background-repeat: inherit;">package singleton

import (
"testing"
)

func TestGetInstance(t *testing.T) {
for day := 0; day < 3; day++ {
// 三天见的皇帝都是同一个人,荣幸吧!
e := GetInstance()
e.Say()
}
}
</pre>

运行结果:

<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="" cid="n32" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--font-monospace); font-size: 0.85rem; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-size: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248) !important; position: relative !important; width: inherit; border: 1px solid rgb(244, 244, 244); -webkit-font-smoothing: initial; line-height: 1.43rem; border-top-left-radius: 2px; border-top-right-radius: 2px; border-bottom-right-radius: 2px; border-bottom-left-radius: 2px; word-wrap: normal; margin: 0.8rem 0px !important; padding: 0.3rem 0px !important; background-position: inherit; background-repeat: inherit;">=== RUN TestGetInstance
我就是皇帝某某某...
我就是皇帝某某某...
我就是皇帝某某某...
--- PASS: TestGetInstance (0.00s)
PASS</pre>

简单分析一下这个例子:为什么皇帝是单例的呢?其原因是init()函数仅在包第一次被加载时执行一次,所以只会创建出一个实例,而我们把emperor声明为包外不可访问的了,所以在包的外部无法通过e := &emperor{}或者e := new(emperor)这种方式创建出新的实例,这就实现了自行实例化并且只有一个实例。

懒汉式与饿汉式

单例模式的一个常见考点就是“懒汉式”与“饿汉式”。那么在Go语言里如何编写呢?

饿汉式

因为饿汉式相对比较好理解一些,代码写起来也更简单,所以我们先讲讲饿汉式。

顾名思义,饿汉很饿,所以它不等你用到实例就先把实例先给创建好了。这种方法不需要加锁,没有线程安全问题,但是会减慢启动速度,且由于在使用之前就创建了实例,所以会浪费一部分内存空间(也就是说不是“按需创建”)。这种方法适用于创建实例使用的资源比较少的场景。

实际上,我们刚刚写的皇帝与臣子的代码就是饿汉式写法的一个例子(使用init()函数)。下面给出饿汉式的通用代码:

写法1:

singleton/singleton_hungry.go

<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="go" cid="n49" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--font-monospace); font-size: 0.85rem; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-size: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248) !important; position: relative !important; width: inherit; border: 1px solid rgb(244, 244, 244); -webkit-font-smoothing: initial; line-height: 1.43rem; border-top-left-radius: 2px; border-top-right-radius: 2px; border-bottom-right-radius: 2px; border-bottom-left-radius: 2px; word-wrap: normal; margin: 0.8rem 0px !important; padding: 0.3rem 0px !important; background-position: inherit; background-repeat: inherit;">package singleton

// 饿汉式写法1: 使用全局变量

var instance1 = &singleton1{}

type singleton1 struct{}

func GetInstance1() *singleton1 {
return instance1
}

// 饿汉式写法2: 使用init函数

var instance2 *singleton2

type singleton2 struct{}

func init() {
instance2 = &singleton2{}
}

func GetInstance2() *singleton2 {
return instance2
}
</pre>

需要注意的是两种写法使用起来差不多,因为虽然全局变量的初始化会比init()函数执行早一点,但都是在main()函数之前,所以在使用上没有特别大的差距,具体选择哪种还是要看实际的业务场景。

懒汉式

有饿汉式自然就会有懒汉式。懒汉式本质上就是按需创建,在你需要用到这个实例的时候才会去创建它。这种方法写起来比较复杂(但也有使用sync.Once的简单写法),可能会产生线程安全问题,适用于创建实例使用的资源较多的场景。

懒汉式有很多种写法,它们是否线程安全也是不一样的,下面来介绍一下这些写法(注:以下所有代码都在singleton/singleton_lazy.go文件中):

写法1:不加锁

<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="go" cid="n72" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--font-monospace); font-size: 0.85rem; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-size: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248) !important; position: relative !important; width: inherit; border: 1px solid rgb(244, 244, 244); -webkit-font-smoothing: initial; line-height: 1.43rem; border-top-left-radius: 2px; border-top-right-radius: 2px; border-bottom-right-radius: 2px; border-bottom-left-radius: 2px; word-wrap: normal; margin: 0.8rem 0px !important; padding: 0.3rem 0px !important; background-position: inherit; background-repeat: inherit;">// 写法1: 不加锁, 线程不安全

var instance3 *singleton3

type singleton3 struct{}

func GetInstance3() *singleton3 {
if instance3 == nil {
instance3 = &singleton3{}
}
return instance3
}</pre>

相信大家都能看出来这种方法是线程不安全的,在并发执行的时候可能会由于多个线程同时判断instance3 == nil成立进而创建多个实例,所以不推荐使用。

写法2:对GetInstance()方法加锁

<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="go" cid="n81" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--font-monospace); font-size: 0.85rem; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-size: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248) !important; position: relative !important; width: inherit; border: 1px solid rgb(244, 244, 244); -webkit-font-smoothing: initial; line-height: 1.43rem; border-top-left-radius: 2px; border-top-right-radius: 2px; border-bottom-right-radius: 2px; border-bottom-left-radius: 2px; word-wrap: normal; margin: 0.8rem 0px !important; padding: 0.3rem 0px !important; background-position: inherit; background-repeat: inherit;">// 写法2: 对GetInstance方法加锁, 线程安全, 但是效率比较低

var (
instance4 *singleton4
lock1 sync.Mutex
)

type singleton4 struct{}

func GetInstance4() *singleton4 {
lock1.Lock()
defer lock1.Unlock()
if instance4 == nil {
instance4 = &singleton4{}
}
return instance4
}
</pre>

由于在多线程并发时GetInstance4()方法只允许一个线程进入,第二个线程需要在第一个线程退出之后才能进入,所以这种方法是线程安全的。但是它也有显而易见的缺点:效率低,因为每次获取实例时都需要加锁解锁。

写法3:创建单例时加锁

<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="go" cid="n89" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--font-monospace); font-size: 0.85rem; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-size: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248) !important; position: relative !important; width: inherit; border: 1px solid rgb(244, 244, 244); -webkit-font-smoothing: initial; line-height: 1.43rem; border-top-left-radius: 2px; border-top-right-radius: 2px; border-bottom-right-radius: 2px; border-bottom-left-radius: 2px; word-wrap: normal; margin: 0.8rem 0px !important; padding: 0.3rem 0px !important; background-position: inherit; background-repeat: inherit;">// 写法3: 创建单例时加锁, 线程不安全, 这种写法仅仅是为了引出写法4

var (
instance5 *singleton5
lock2 sync.Mutex
)

type singleton5 struct{}

func GetInstance5() *singleton5 {
if instance5 == nil {
lock2.Lock()
instance5 = &singleton5{}
lock2.Unlock()
}
return instance5
}
</pre>

这种方法也是线程不安全的。虽然同一时刻只可能有一个线程在执行instance5 = &singleton5{}这行代码,但是仍然有可能有多个线程都判断instance5 == nil成立并创建多个对象。它本质上跟不加锁没什么区别,提及这种写法仅仅是为了引出下面的写法4:双重检查机制。

写法4:双重检查

<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="go" cid="n95" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--font-monospace); font-size: 0.85rem; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-size: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248) !important; position: relative !important; width: inherit; border: 1px solid rgb(244, 244, 244); -webkit-font-smoothing: initial; line-height: 1.43rem; border-top-left-radius: 2px; border-top-right-radius: 2px; border-bottom-right-radius: 2px; border-bottom-left-radius: 2px; word-wrap: normal; margin: 0.8rem 0px !important; padding: 0.3rem 0px !important; background-position: inherit; background-repeat: inherit;">// 写法4: 双重检查, 线程安全

var (
instance6 *singleton6
lock3 sync.Mutex
)

type singleton6 struct{}

func GetInstance6() *singleton6 {
if instance6 == nil {
lock3.Lock()
if instance6 == nil {
instance6 = &singleton6{}
}
lock3.Unlock()
}
return instance6
}
</pre>

这是一种线程安全的写法。既然有可能有多个线程同时判断instance6 == nil,那么再加锁之后再检查一次就行了。但是每一次获取实例都要加锁还要检查两次显然不是一个明智的选择,所以我们有更优的解法:使用sync.Once

写法5:使用sync.Once

<pre spellcheck="false" class="md-fences mock-cm md-end-block" lang="go" cid="n101" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--font-monospace); font-size: 0.85rem; display: block; break-inside: avoid; text-align: left; white-space: pre-wrap; background-image: inherit; background-size: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248) !important; position: relative !important; width: inherit; border: 1px solid rgb(244, 244, 244); -webkit-font-smoothing: initial; line-height: 1.43rem; border-top-left-radius: 2px; border-top-right-radius: 2px; border-bottom-right-radius: 2px; border-bottom-left-radius: 2px; word-wrap: normal; margin: 0.8rem 0px !important; padding: 0.3rem 0px !important; background-position: inherit; background-repeat: inherit;">// 写法5: 使用sync.Once, 线程安全, 推荐使用

var (
instance7 *singleton7
once sync.Once
)

type singleton7 struct{}

func GetInstance7() *singleton7 {
once.Do(func() {
instance7 = &singleton7{}
})
return instance7
}
</pre>

sync.Once有点类似于init()函数,它们都执行且仅执行一次,区别在于sync.Once是在你需要的时候执行,而init()是在包第一次被加载的时候执行。那为什么sync.Once可以解决加锁的问题呢?这就跟sync.Once的内部实现有关了。

以下是sync.Once的源码,非常短,但是很有参考价值:

<pre spellcheck="false" class="md-fences mock-cm md-end-block" lang="go" cid="n107" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--font-monospace); font-size: 0.85rem; display: block; break-inside: avoid; text-align: left; white-space: pre-wrap; background-image: inherit; background-size: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248) !important; position: relative !important; width: inherit; border: 1px solid rgb(244, 244, 244); -webkit-font-smoothing: initial; line-height: 1.43rem; border-top-left-radius: 2px; border-top-right-radius: 2px; border-bottom-right-radius: 2px; border-bottom-left-radius: 2px; word-wrap: normal; margin: 0.8rem 0px !important; padding: 0.3rem 0px !important; background-position: inherit; background-repeat: inherit;">type Once struct {
done uint32
m Mutex
}

func (o *Once) Do(f func()) {
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 {
defer atomic.StoreUint32(&o.done, 1)
f()
}
}
</pre>

可以发现Do()函数中仅仅做了一次判断——如果传入的函数已经执行了(done的值为1),那么就不执行,直接返回;否则执行doSlow()方法。在doSlow()方法中进行了加锁并执行了传入的函数,在代码块运行结束后再把done修改为1,这样就实现了执行且仅执行一次的功能,并且只有第一次需要加锁,这样对于GetInstance()函数来说就不再需要判断instance是否为nil了,也不再需要手动进行加锁解锁操作了,可谓是非常棒的一种解决方案。

总结

Go语言实现单例模式还是挺简单的,基本上看一遍就能懂(从Java转到Go的我表示:比的Java简单多了!尤其是sync.Once写法,精彩程度堪比Java单例模式的enum写法),但要注意转变思维——因为Go语言本身的特点,它的单例模式写法与其他语言(Java、C++等)有很大的区别,如果是初学者自然不用在意这个,但是对于有其他语言基础的还是应该注意一下。

Reference

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,457评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,837评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,696评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,183评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,057评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,105评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,520评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,211评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,482评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,574评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,353评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,897评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,489评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,683评论 2 335

推荐阅读更多精彩内容

  • 单例模式在开发中是一种相对简单的设计模式,但它在实现上又有很多种方式 熟悉java的同学知道在java中实现单例常...
    星丶雲阅读 365评论 0 0
  • 单例模式(SingletonPattern)一般被认为是最简单、最易理解的设计模式,也因为它的简洁易懂,是项目中最...
    成热了阅读 4,196评论 4 34
  • 单例模式有很多好处,它能够避免实例对象的重复创建,不仅可以减少每次创建对象的时间开销,还可以节约内存空间;能够避免...
    修罗掌柜阅读 813评论 0 3
  • 我是黑夜里大雨纷飞的人啊 1 “又到一年六月,有人笑有人哭,有人欢乐有人忧愁,有人惊喜有人失落,有的觉得收获满满有...
    陌忘宇阅读 8,503评论 28 53
  • 首先介绍下自己的背景: 我11年左右入市到现在,也差不多有4年时间,看过一些关于股票投资的书籍,对于巴菲特等股神的...
    瞎投资阅读 5,626评论 3 8