前言

在咱们开发过程中常常会应用到单例模式这一经典的设计模式,单例模式能够帮忙开发者针对某个(些)变量或者对象或者函数(办法)进行在程序运行期间只有一次的初始化或者函数调用操作,比方在开发我的项目中针对某一类连接池的初始化(如数据库连接池等)。针对这种状况,咱们就须要应用单例模式进行操作。

单例模式

本人搞得单例模式

要实现一个单例模式,咱们会很快就想到了在一个构造体中搁置一个flag字段用于标记以后的函数是否被执行过,举个:

`type SingletonPattern struct {` `done bool``}``func (receiver *SingletonPattern) Do(f func())  {` `if !receiver.done {` `f()` `receiver.done=true` `}``}`

看似很美妙,然而此时,如果传入的须要调用的函数f()会执行很长时间,比方数据库查问或者做一些连贯什么的,当别的goroutine运行到此处的时候因为还没有执行完f(),就会发现done标记依然是false,那么依然会调用一次f(),此时就违反了单例模式的初衷。

那么如何解决下面的并发的问题呢。此时就能够应用go规范库中所提供的并发原语---sync.Once

规范库真香系列之sync.Once

话不多说先上sync.Once 构造体的源代码:

`type Once struct {` `// 标记符号,用于标记是否执行过` `done uint32` `// 互斥锁,用于爱护并发调用以及避免copy` `m    Mutex``}`

构造体就这么简略,字段done用于标记是否执行过函数,至于为什么应用uint32类型,作者的了解是为了之后应用atomic操作做的斗争,m字段值用于爱护并发状况下的情景,并且因为继承了Locker接口能够通过vet校验到其是否被复制

接下来看一下用于执行函数调用的Do()函数的实现:

`func (o *Once) Do(f func()) {` `// 原子获取以后 done 字段是否等于0` `// 如果以后字段等于1`  `// 则代表曾经 执行过` `// 这是第一层校验` `if atomic.LoadUint32(&o.done) == 0 {` `// 如果为0则代表没被调用过则调用` `// 此处写成一个函数的起因是为了` `// 进行函数内联晋升性能` `o.doSlow(f)` `}``}``func (o *Once) doSlow(f func()) {` `// 此处加锁用于避免其余goroutine同时拜访调用` `o.m.Lock()` `defer o.m.Unlock()` `// 二次校验` `// 为的是避免多个goroutine进入此函数的时候,可能产生的反复执行 f()` `if o.done == 0 {` `// 函数执行完结设置done 字段为 1代表曾经执行结束` `defer atomic.StoreUint32(&o.done, 1)` `// 执行` `f()` `}``}`

此时,sync.Once 的所有源代码曾经解析结束了(惊不惊喜,意不意外),其实sync.Once 的过程很简略,就是依据标记进行双重判断确定函数是否执行过,没执行就执行,执行了就跳过。

sync.Once 的应用问题

哪来的deadlock?

sync.Once 确实很简略,应用也很简略,然而还是会有应用上可能呈现的一些问题比方下列代码:

`func main() {` `var once sync.Once` `once.Do(` `func() {` `fmt.Println("one once do")` `once.Do(` `func() {` `fmt.Println("second once do")` `})` `})``}`

该代码会呈现什么问题?答案是:

fatal error: all goroutines are asleep - deadlock!

为什么会这样?因为内层个Do是被外层的同一个once对象所调用,因为此时曾经进入了第一个Do并且曾经调用了函数,那么此时sync.Once 中的互斥锁字段,曾经被加了锁,此时二次加锁就会产生死锁。因而应用sync.Once 最重要的一点就是:*

不要在执行函数中,嵌套以后的sync.Once 对象 不要在执行函数中,嵌套以后的sync.Once 对象 不要在执行函数中,嵌套以后的sync.Once 对象。(重要的话要说三遍)

哪来的invalid memory address or nil pointer dereference?

看一下上面的代码:

`func main() {` `var once sync.Once` `var conn net.Conn` `once.Do(` `func() {` `var err error` `conn, err = net.Dial("tcp", "")` `if err != nil {` `return` `}` `})` `conn.RemoteAddr()``}`

在运行时,会呈现:

panic: runtime error: invalid memory address or nil pointer dereference

为什么?因为sync.Once只保障执行一次,然而不保障执行是否出错,即我只管调用,出错了跟我无关,上述代码中

`conn, err = net.Dial("tcp", "")`

必然呈现err!=nil的状况,此时如果不对conn变量进行判断为nil,就会呈现空指针异样,那么,如何来保障他执行胜利了呢,咱们须要对其进行革新

`type Once struct {` `once sync.Once``}``func (receiver *Once) OnceDo(f func() error) error {` `var err error` `receiver.once.Do(` `func() {` `err = f()` `})` `return err``}``func main() {` `var once Once` `var conn net.Conn` `err := once.OnceDo(` `func() error {` `var err error` `conn, err = net.Dial("tcp", "")` `if err != nil {` `return err` `}` `return nil` `})` `if err != nil {` `log.Fatal(err)` `}``}`

通过封装,咱们就能够失去sync.Once 执行时是否出错,以适配各种错误处理。

此封装可能会有更好的解决方案,下面的计划也仅仅是一个罢了。

总结

至此sync.Once 的用法以及源码解析就实现了,可能有些中央有些了解上的谬误,请各位体谅并且帮忙指出修改意见,如果这篇文章能帮到你,这是我的荣幸。