文章目录
一、defer的简单使用
defer
拥有注册延迟调用的机制,**
defer
关键字后面跟随的语句或者函数,会在当前的函数
return
正常结束 或者
panic
异常结束 后执行。**
但是
defer
只有在注册后,最后才能生效调用执行,
return
之后的
defer
语句是不会执行的,因为并没有注册成功。
如下例子:
funcmain(){deferfunc(){
fmt.Println(111)}()
fmt.Println(222)returndeferfunc(){
fmt.Println(333)}()}
执行结果:
222111
解析:
222
、
111
是在
return
之前注册的,所以如期执行,
333
是在
return
之后注册的,注册失败,执行不了。
defer
在需要资源释放的场景非常有用,可以很方便地在函数结束前执行一些操作。
比如在 打开连接/关闭连接 、加锁/释放锁、打开文件/关闭文件 这些场景下:
file, err := os.Open("1.txt")if err !=nil{panic(err)}if file !=nil{defer file.Close()}
这里要注意的是:在调用
file.Close()
之前,需要判断
file
是否为空,避免出现异常情况。
再来看一个错误示范,没有正确使用
defer
的例子:
player.mu.Lock()
rand.Intn(number)
player.mu.Unlock()
这三行代码,存在两个问题:
- 中间这行代码
是有可能发生rand.Intn(number)
的,这就会导致没有正常解锁。panic
- 这样的代码在项目中后续可能被其他人修改,在
后增加更多的逻辑,这是完全不可控的。rand.Intn(number)
在
Lock
和
Unlock
之间的代码一旦出现
panic
,就会造成死锁。因此,即使逻辑非常简单,使用
defer
也是很有必要的,因为需求总在变化,代码也总会被修改。
二、defer的函数参数与闭包引用
defer
延迟语句不会马上执行,而是会进入一个栈,函数
return
前,会按先进后出的顺序执行。
先进后出的原因是后面定义的函数可能会依赖前面的资源,自然要先执行;否则,如果前面的先执行了,那么后面函数的依赖就没有了,就可能会导致出错。
在
defer
函数定义时,对外部变量的引用有三种方式:值传参、指针传参、闭包引用。
- 值传参:在
defer
定义时就把值传递给defer
,并复制一份cache起来,defer调用时和定义的时候值是一致的。 - 指针传参:在
defer
定义时就把指针传递给defer
,defer调用时根据整个上下文确定参数当前的值。 - 闭包引用:在
defer
定义时就把值引用传递给defer
,defer调用时根据整个上下文确定参数当前的值。
下面通过例子加深一下理解。
例子1:
funcmain(){var arr [4]struct{}for i :=range arr {deferfunc(){
fmt.Println(i)}()}}
执行结果:
3333
解析:因为
defer
后面跟着的是一个闭包,根据整个上下文确定,
for
循环结束后
i
的值为3,因此最后打印了4个3。
例子2:
funcmain(){var n int// 值传参deferfunc(n1 int){
fmt.Println(n1)}(n)// 指针传参deferfunc(n2 *int){
fmt.Println(*n2)}(&n)// 闭包deferfunc(){
fmt.Println(n)}()
n =4}
执行结果:
440
解析:
defer
执行顺序和定义的顺序是相反的;
第三个
defer
语句是闭包,引用的外部变量
n
,defer调用时根据上下文确定,最终结果是4;
第二个
defer
语句是指针传参,defer调用时根据整个上下文确定参数当前的值,最终结果是4;
第一个
defer
语句是值传参,defer调用时和定义的时候值是一致的,最终结果是0;
例子3:
funcmain(){// 文件1
f,_:= os.Open("1.txt")if f !=nil{deferfunc(f io.Closer){if err := f.Close(); err !=nil{
fmt.Printf("defer close file err 1 %v\n", err)}}(f)}// 文件2
f,_= os.Open("2.txt")if f !=nil{deferfunc(f io.Closer){if err := f.Close(); err !=nil{
fmt.Printf("defer close file err 2 %v\n", err)}}(f)}
fmt.Println("success")}
执行结果:
success
解析:先说结论,这个例子的代码没有问题,两个文件都会被成功关闭。这个是对
defer
原理的应用,因为
defer
函数在定义的时候,参数就已经复制进去了,这里是值传参,真正执行
close()
函数的时候就刚好关闭的是正确的文件。如果不把
f
当做值传参,最后两个
close()
函数关闭的就是同一个文件了,都是最后打开的那个文件。
例子3的错误示范:
funcmain(){// 文件1
f,_:= os.Open("1.txt")if f !=nil{deferfunc(){if err := f.Close(); err !=nil{
fmt.Printf("defer close file err 1 %v\n", err)}}()}// 文件2
f,_= os.Open("2.txt")if f !=nil{deferfunc(){if err := f.Close(); err !=nil{
fmt.Printf("defer close file err 2 %v\n", err)}}()}
fmt.Println("success")}
执行结果:
success
deferclose file err 1close2.txt: file already closed
例子4:
// 值传参funcfunc1(){var err errordefer fmt.Println(err)
err = errors.New("func1 error")return}// 闭包funcfunc2(){var err errordeferfunc(){
fmt.Println(err)}()
err = errors.New("func2 error")return}// 值传参funcfunc3(){var err errordeferfunc(err error){
fmt.Println(err)}(err)
err = errors.New("func3 error")return}// 指针传参funcfunc4(){var err errordeferfunc(err *error){
fmt.Println(*err)}(&err)
err = errors.New("func4 error")return}funcmain(){func1()func2()func3()func4()}
执行结果:
<nil>
func2 error<nil>
func4 error
解析:
第一个和第三个函数中,都是作为参数,进行值传参,
err
在定义的时候就会求值,因为定义的时候值都是
nil
,所以最后的结果都是
nil
;
第二个函数的参数在定义的时候也求值了,但是它是个闭包,查看上下文发现最后值被修改为
func2 error
;
第四个函数是指针传参,最后值被修改为
func4 error
;
**现实中,第三个函数闭包的例子是比较容易犯的错误,导致最后
defer
语句没有起到作用,造成生产上的事故,需要特别注意。**
三、defer的语句拆解
从返回值出发来拆解延迟语句
defer
。
return xxx
这条语句经过编译之后,实际上生成了三条指令:
1. 返回值 = xxx
2. 调用 defer 函数
3. 空的 return
其中,
1
和
3
是
return
语句生成的指令,
2
是
defer
语句生成的指令。可以看出:
**
return
并不是一条原子指令;
defer
语句在第二步调用,这里可能操作返回值,从而影响最终结果。**
接下来通过例子来加深理解。
例子1:
funcfunc1()(r int){
t :=3deferfunc(){
t = t +3}()return t
}funcmain(){
r :=func1()
fmt.Println(r)}
执行结果:
3
语句拆解:
funcfunc1()(r int){
t :=3// 1.返回值=xxx:赋值指令
r = t
// 2.调用defer函数:defer在赋值与返回之前执行,这个例子中返回值r没有被修改过func(){
t = t +3}()// 3.空的returnreturn}funcmain(){
r :=func1()
fmt.Println(r)}
解析:因为第二个步骤里并没有操作返回值
r
,所以最终得到的结果是
3
。
例子2:
funcfunc2()(r int){deferfunc(r int){
r = r +3}(r)return1}funcmain(){
r :=func2()
fmt.Println(r)}
执行结果:
1
语句拆解:
funcfunc2()(r int){// 1.返回值=xxx:赋值指令
r =1// 2.调用defer函数:因为是值传参,所以修改的r是个复制的值,不会影响要返回的那个r值。func(r int){
r = r +3}(r)// 3.空的returnreturn}funcmain(){
r :=func2()
fmt.Println(r)}
解析:因为第二个步骤里改变的是传值进去的
r
值,是一个形参的复制值,不会影响实参
r
,所以最终得到的结果是
1
。
例子3:
funcfunc3()(r int){deferfunc(){
r = r +3}()return1}funcmain(){
r :=func3()
fmt.Println(r)}
执行结果:
4
语句拆解:
funcfunc3()(r int){// 1.返回值=xxx:赋值指令
r =1// 2.调用defer函数:因为是闭包,捕获的变量是引用传递,所以会修改返回的那个r值。func(){
r = r +3}()// 3.空的returnreturn}funcmain(){
r :=func3()
fmt.Println(r)}
解析:因为第二个步骤里改变的
r
值是闭包,闭包中捕获的变量是引用传递,不是值传递,所以最终得到的结果是
4
。
四、defer中的recover
代码中的
panic
最终会被
recover
捕获到。在日常开发中,可能某一条协议的逻辑触发了某一个
bug
造成
panic
,这时就可以用
recover
去捕获
panic
,稳住主流程,不影响其他协议的业务逻辑。
需要注意的是,**
recover
函数只在
defer
的函数中直接调用才生效。**
通过例子看
recover
调用情况。
例子1:
funcfunc1(){if err :=recover(); err !=nil{
fmt.Println("func1 recover", err)return}}funcmain(){deferfunc1()panic("func1 panic")}
执行结果:
func1 recover func1 panic
解析:正确
recover
,因为在
defer
中调用的,所以可以生效。
例子2:
funcmain(){recover()panic("func2 panic")}
执行结果:
panic: func2 panic
goroutine 1[running]:
main.main()
C:/Users/ycz/go/ccc.go:5+0x31
exit status 2
解析:错误
recover
,直接调用
recover
,返回
nil
。
例子3:
funcmain(){deferrecover()panic("func3 panic")}
执行结果:
panic: func3 panic
goroutine 1[running]:
main.main()
C:/Users/ycz/go/ccc.go:5+0x65
exit status 2
解析:错误
recover
,
recover
需要在
defer
的函数里调用。
例子4:
funcmain(){deferfunc(){deferfunc(){recover()}()}()panic("func4 panic")}
执行结果:
panic: func4 panic
goroutine 1[running]:
main.main()
C:/Users/ycz/go/ccc.go:9+0x49
exit status 2
解析:错误
recover
,不能在多重
defer
嵌套里调用
recover
。
另外需要注意的一点是,**父
goroutine
无法
recover
住 子
goroutine
的
panic
。**
原因是,
goroutine
被设计为一个独立的代码执行单元,拥有自己的执行栈,不与其他
goroutine
共享任何的数据。
也就是说,无法让
goroutine
拥有返回值,也无法让
goroutine
拥有自身的
ID
编号。
如果希望有一个全局的
panic
捕获中心,那么可以通过
channel
来实现,如下示例:
var panicNotifyManage chaninterface{}funcStartGlobalPanicRecover(){
panicNotifyManage =make(chaninterface{})gofunc(){select{case err :=<-panicNotifyManage:
fmt.Println("panicNotifyManage--->", err)}}()}funcGoSafe(f func()){gofunc(){deferfunc(){if err :=recover(); err !=nil{
panicNotifyManage <- err
}}()f()}()}funcmain(){StartGlobalPanicRecover()
f1 :=func(){panic("f1 panic")}GoSafe(f1)
time.Sleep(time.Second)}
解析:
GoSafe()
本质上是对
go
关键字进行了一层封装,确保在执行并发单元前插入一个
defer
,从而保证能够
recover
住
panic
。但是这个方案并不完美,如果开发人员不使用
GoSafe
函数来创建
goroutine
,而是自己创建,并且在代码中出现了
panic
,那么仍然会造成程序崩溃。
版权归原作者 hcraM41 所有, 如有侵权,请联系我们删除。