写在前面
单例模式是最常用的设计模式之一,虽然简单,但是还是有一些小坑点需要注意。本文介绍单例模式并使用go语言实现一遍单例模式。
单例模式介绍
简介
单例模式保证一个类仅有一个实例,并提供一个访问它的全局访问点。
使用场景:
- 当类只能有一个实例而且可以从一个公开的众所周知的访问点访问它。
- 当这个唯一实例是通过子类化可扩展的,并且应该无须更改单例代码就能使用一个扩展的实例。
结构图
分类
单例里面分成了两种模式,一种是饿汉式,一种是懒汉式。
饿汉式
饿汉模式下的单例,只在使用实例的时候就创建了对象。
代码实例:
var req *Request
type Request struct {
}
func NewRequest() *Request {
if req == nil {
req = new(Request)
}
return req
}
这个代码看起来没什么问题,但是这段代码是线程不安全
的。 因为如果有多个请求过来,就会同时创建多个对象,就会出现频繁的创建和删除对象,给gc造成很大的压力。
当然我们也可以加锁,但是频繁的加锁,解锁会严重影响系统的性能。
var req *Request
var lock sync.Mutex
type Request struct {
}
func NewRequest() *Request {
lock.Lock()
defer lock.Unlock()
if req == nil {
req = new(Request)
}
return req
}
每一个对象的创建,都需要加锁解锁,非常影响性能
懒汉式
懒汉式的单例模式可以解决我们饿汉式的缺点,并且是线程安全的,保证了我们对象全局唯一并且不会重复创建。
代码如下:
var req *Request
var reqOnce sync.Once
type Request struct {
}
func NewRequest() *Request {
reqOnce.Do(func() {
req = &Request{}
})
return req
}
这里我们用到go语言的 sync.Once 包,而sync.Once的作用就是使得我们的函数只执行一次。并且是并发安全的。
那为什么是并发安全的呢?接下来我们详解一下 sync.Once
sync.Once
sync.once 内部原理很简单,Once的结构题如下:
type Once struct {
// done变量用来标识函数是否执行完毕
done uint32
// m用来保证函数执行期间,其他goroutine阻塞
m Mutex
}
这个Do方法是判断有无执行过这个该方法,并且是使用atomic.LoadUint32
的原子操作。
func (o *Once) Do(f func()) {
// 1.判断函数是否执行过,done=0说明没执行过
if atomic.LoadUint32(&o.done) == 0 {
o.doSlow(f)
}
}
-
如果没执行过就执行这个方法,那么这里会有一个double check的工作,其实就是防止可能在上一次检查到这一次加锁加完的时间段间隙上有人执行完了这个函数
-
确认确实没有执行的时候,就执行函数,并进行原子操作标识,将 done 置为1。
func (o *Once) doSlow(f func()) {
// 1.加锁
o.m.Lock()
// 5.函数返回后,释放锁
defer o.m.Unlock()
// 2.判断是否执行过
if o.done == 0 {
// 4.函数返回后,设置执行标识
defer atomic.StoreUint32(&o.done, 1)
// 3.具体执行函数
f()
}
}
应用场景举例子
我们可以在 service 上定义某一个方法的结构体函数,并将可以根据这个结构体函数进行方法的扩展。
在controll层只需要调用这个函数方法即可GetUserSrv()