单例模式大家都比较了解,定义如下:一个类只允许创建唯一一个对象(或者实例),那这个类就是一个单例类,这种设计模式就叫作单例设计模式,简称单例模式。
单例模式虽然理解起来比较简单,但是真正实现的时候有很多细节需要考虑,一般考虑点有如下几个:
1.构造函数需要是 private 访问权限的,这样才能避免外部通过 new 创建实例
2.考虑对象创建时的线程安全问题
3.考虑是否支持延迟加载
4.考虑 getInstance() 性能是否高(是否加锁)
代码
语法不同,对于这几点关注程度也不同。对于Go语言,在这里提供一种写法,使用sync.Once.Do。该函数的作用是只执行一次。
所以我们可以这么写:
1 | type Single struct { |
无论多少请求,只会有一个Single的实例。
测试
现在我们思考一个场景,如果突然有100个请求同时请求GetSingleInstance接口,这些请求是会等Do执行完还是无视Do直接return single呢?
理论上是需要等Do执行完的,否则返回的single为空,会导致严重错误。虽然是这么想,不过还是做个测试吧。
1 | package main |
测试方案很简单,启动5个goroutine,同时调用Do,onceBody设置sleep 5秒,只要检查输出,就能判断是否会阻塞。
执行结果如下:
➜ myproject go run main.go
finished
0
Only once start
2
4
3
1
Only once end
lll2
lll4
lll0
lll1
lll3
可以看出,只有Do执行完毕后,所有goroutine才会输出,证明Do都会被调用,但只有一个会真正执行,在真正的执行完前,其它goroutine会被阻塞。
其实这里有一个隐藏风险,如果Do执行的函数很耗时,会导致大量goroutine累积,编程的时候需要考虑到这一点。
具体实现
Do这个功能是如何实现的呢?让我们看一下源码:
1 | func (o *Once) Do(f func()) { |
多个协程查看done值为0,进入doSlow,只有一个协程会获得锁,其它协程别阻塞。
其实用到的都是比较常规的技术,主要是互斥锁、信号量、defer,但是设计上还是很巧妙的。这也是Go的一个优势,解决冲突使用锁,快速、安全、方便,但是需要考虑好性能问题。