0


Golang defer延迟语句详解

文章目录

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

这三行代码,存在两个问题:

  1. 中间这行代码
     rand.Intn(number)
    
    是有可能发生
    panic
    
    的,这就会导致没有正常解锁。
  2. 这样的代码在项目中后续可能被其他人修改,在
    rand.Intn(number)
    
    后增加更多的逻辑,这是完全不可控的。

Lock

Unlock

之间的代码一旦出现

panic

,就会造成死锁。因此,即使逻辑非常简单,使用

defer

也是很有必要的,因为需求总在变化,代码也总会被修改。


二、defer的函数参数与闭包引用

defer

延迟语句不会马上执行,而是会进入一个栈,函数

return

前,会按先进后出的顺序执行。

先进后出的原因是后面定义的函数可能会依赖前面的资源,自然要先执行;否则,如果前面的先执行了,那么后面函数的依赖就没有了,就可能会导致出错。

defer

函数定义时,对外部变量的引用有三种方式:值传参、指针传参、闭包引用。

  1. 值传参:在defer 定义时就把值传递给defer ,并复制一份cache起来,defer调用时和定义的时候值是一致的。
  2. 指针传参:在defer 定义时就把指针传递给defer ,defer调用时根据整个上下文确定参数当前的值。
  3. 闭包引用:在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

,那么仍然会造成程序崩溃。


本文转载自: https://blog.csdn.net/hcraM41/article/details/140594224
版权归原作者 hcraM41 所有, 如有侵权,请联系我们删除。

“Golang defer延迟语句详解”的评论:

还没有评论