go的单例模式

编程

<!-- more -->

var instance *single

type single struct {

Name string

}

func GetInstance() *single {

if instance == nil {

instance = &single{}

}

return instance

}

func main() {

i := GetInstance()

i.Name = "1"

i1 := GetInstance()

i1.Name = "1"

fmt.Printf("%p

", i) // 0xc000094040

fmt.Printf("%p

", i1) // 0xc000094040

}

从上面例子打印结果可以看出对象指向的地址都是一样的,所以判断i和i1是同一个对象。有个需要注意的地方single是小写的,如果是大写包外可见,就可以实例化single对象,不符合单例模式只有一个实例的特点。

但是这么写单协程是没问题的,多协程就会出问题。于是利用go的同步原语sync来加锁。

var instance *single

var lock sync.Mutex

type single struct {

Name string

}

func GetInstance() *single {

lock.Lock()

defer lock.Unlock()

if instance == nil {

instance = &single{}

}

return instance

}

// 上面每次获取实例都会加锁,优化下,就是我们常见的双重检查机制,只有在没有实例化的情况下再加锁

func GetInstance() *single {

if instance == nil {

lock.Lock()

defer lock.Unlock()

if instance == nil {

instance = &single{}

}

}

return instance

}

上面利用加锁来达到协程安全的单例模式,利用sync.Mutex同步原语实际上是利用临界区的方式,go还有利用sync.Once实现单例:

var instance *single

var once sync.Once

type single struct {

Name string

}

func GetInstance() *single {

once.Do(func() {

instance = &single{}

})

return instance

}

至此我们单例模式就讲完了,现在再讲sync.Once, Once实际上是只计算Do方法调用的次数,并且Do内的函数只执行一次。那么Once是怎么做到的呢,源码很简单:

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()

}

}

sync.Once是一个结构体,内部定义了一个锁和一个类似标志的done,通过对done值的计算,判断done为0时sync.Once的Do的函数才执行一次,否则不执行。

我们看到Do方法中利用atomic.LoadInt32接受一个*int32类型的指针值,并会返回该指针值指向的那个值,atomic是利用CAS并不是加锁,这种读多的场景利用CAS的方式也是性能上的优化。而在doSlow利用了atomic将done+1,但是CAS在频繁写的场景中并不是总是能成功,所以这里为了保险起见还是加了锁。

从几种单例模式讲到sync.Once,总体来说利用sync.Once更优雅,性能上也更好些。

以上是 go的单例模式 的全部内容, 来源链接: utcz.com/z/513925.html

回到顶部