前言
最近写着写着遇到些稀奇古怪的问题,想着与defer、GOTO以及锁有关,于是整理了一下。
切入正题
首先来看看本文的主角们
十分好用的defer
1 | func main() { |
这是一个defer的用例,很简单,就是在doSomething结束之后,再执行finishDOsomething(println输出到stderr,fmt.Println输出到stdout,我绝对不是为了偷懒少打点才用的),以上代码的输出如下:println
1 | Do something |
没有任何问题,十分正常且符合预期的输出。
掺和一下goto,顺便加了点料
现在笔者想在doSomething结束后,从头来过,再来一遍,为了便于区分,顺便加了点其他料:
1 | func main() { |
此时程序的输出为
1 | Do something 1 time |
前五行看下来似乎都没什么问题,着重看最后两行,为什么runningTimes变成2之后仍旧打出了Finish do something 1 time呢?
这里其实与goto没有任何关系,是defer的特性,当程序经过defer时,会进行参数预计算,也就是说,当runTimes是1时,加载到defer finishDoSomething(runningTimes)的时候,就会把finishDoSomething(1)丢到栈里,所以最后可以打印出来Finish do something 1 time。
然后是打印次序及顺序的问题,把刚刚的执行过程用代码重新梳理一下可能就会好理解地多:
1 | doSomething(1) |
所以,goto 之前有defer的照样会执行,很显然,在这个例子里执行了两次defer(当然预计算的值不一样)。
此时一个人畜无害的Mutex混入其中
此时,出于某种原因,在doSomething的时候,必须对locker进行锁定,好让其他人不能同时一起doSomething,稍加修改:
1 | func main() { |
此时执行此程序,似乎出现了不得了的问题(经典故弄玄虚环节)
1 | Do something 1 time |
竟然出现了死锁!(似乎也不必这么惊讶)
这里面的死锁其实是由于锁重入导致的,Golang是不支持重入锁的,这就会导致死锁。
已知了原因似乎也很好解决了,大不了goto前解锁呗:
1 | func main() { |
于是梅开二度:
1 | Do something 1 time |
确实,解决了重入问题,但是没有解决Unlock两次的问题。
从程序跑通的角度来看,利用defer的特性再加一个defer locker.Lock()便可解君愁了:
1 | func main() { |
成功了!它跑起来了!开心!(棒读)
原本加locker的目的在于执行本段代码的时候,有且只有一个goroutine在执行,但是在上述的修改中,更加积重难返,为了Unlock特意Lock这种操作其实大可不必,到这种程度了,修改原来代码的逻辑其实是更好的选择,比如:
1 | func main(){ |
又或者您坚持需要一个可以重入的锁建议自己写一个,可以参考一下以下解决方案:
Go语言如何实现可重入锁? - SegmentFault 思否