前言
在我们开发过程中经常会使用到单例模式这一经典的设计模式,单例模式可以帮助开发者针对某个(些)变量或者对象或者函数(方法)进行在程序运行期间只有一次的初始化或者函数调用操作,比如在开发项目中针对某一类连接池的初始化(如数据库连接池等)。针对这种情况,我们就需要使用单例模式进行操作。
单例模式🌰
自己搞得单例模式
要实现一个单例模式,我们会很快就想到了在一个结构体中放置一个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
的用法以及源码解析就完成了,可能有些地方有些理解上的错误,请各位谅解并且帮忙指出修改意见,如果这篇文章能帮到你,这是我的荣幸。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。