0


Golang defer延迟语句详解

文章目录

一、defer的简单使用

  1. defer

拥有注册延迟调用的机制,**

  1. defer

关键字后面跟随的语句或者函数,会在当前的函数

  1. return

正常结束 或者

  1. panic

异常结束 后执行。**

但是

  1. defer

只有在注册后,最后才能生效调用执行,

  1. return

之后的

  1. defer

语句是不会执行的,因为并没有注册成功。

如下例子:

  1. funcmain(){deferfunc(){
  2. fmt.Println(111)}()
  3. fmt.Println(222)returndeferfunc(){
  4. fmt.Println(333)}()}

执行结果:

  1. 222111

解析:

  1. 222

  1. 111

是在

  1. return

之前注册的,所以如期执行,

  1. 333

是在

  1. return

之后注册的,注册失败,执行不了。

  1. defer

在需要资源释放的场景非常有用,可以很方便地在函数结束前执行一些操作。

比如在 打开连接/关闭连接 、加锁/释放锁、打开文件/关闭文件 这些场景下:

  1. file, err := os.Open("1.txt")if err !=nil{panic(err)}if file !=nil{defer file.Close()}

这里要注意的是:在调用

  1. file.Close()

之前,需要判断

  1. file

是否为空,避免出现异常情况。

再来看一个错误示范,没有正确使用

  1. defer

的例子:

  1. player.mu.Lock()
  2. rand.Intn(number)
  3. player.mu.Unlock()

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

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

  1. Lock

  1. Unlock

之间的代码一旦出现

  1. panic

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

  1. defer

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


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

  1. defer

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

  1. return

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

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

  1. defer

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

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

下面通过例子加深一下理解。

例子1:

  1. funcmain(){var arr [4]struct{}for i :=range arr {deferfunc(){
  2. fmt.Println(i)}()}}

执行结果:

  1. 3333

解析:因为

  1. defer

后面跟着的是一个闭包,根据整个上下文确定,

  1. for

循环结束后

  1. i

的值为3,因此最后打印了4个3。

例子2:

  1. funcmain(){var n int// 值传参deferfunc(n1 int){
  2. fmt.Println(n1)}(n)// 指针传参deferfunc(n2 *int){
  3. fmt.Println(*n2)}(&n)// 闭包deferfunc(){
  4. fmt.Println(n)}()
  5. n =4}

执行结果:

  1. 440

解析:

  1. defer

执行顺序和定义的顺序是相反的;

第三个

  1. defer

语句是闭包,引用的外部变量

  1. n

,defer调用时根据上下文确定,最终结果是4;

第二个

  1. defer

语句是指针传参,defer调用时根据整个上下文确定参数当前的值,最终结果是4;

第一个

  1. defer

语句是值传参,defer调用时和定义的时候值是一致的,最终结果是0;

例子3:

  1. funcmain(){// 文件1
  2. f,_:= os.Open("1.txt")if f !=nil{deferfunc(f io.Closer){if err := f.Close(); err !=nil{
  3. fmt.Printf("defer close file err 1 %v\n", err)}}(f)}// 文件2
  4. f,_= os.Open("2.txt")if f !=nil{deferfunc(f io.Closer){if err := f.Close(); err !=nil{
  5. fmt.Printf("defer close file err 2 %v\n", err)}}(f)}
  6. fmt.Println("success")}

执行结果:

  1. success

解析:先说结论,这个例子的代码没有问题,两个文件都会被成功关闭。这个是对

  1. defer

原理的应用,因为

  1. defer

函数在定义的时候,参数就已经复制进去了,这里是值传参,真正执行

  1. close()

函数的时候就刚好关闭的是正确的文件。如果不把

  1. f

当做值传参,最后两个

  1. close()

函数关闭的就是同一个文件了,都是最后打开的那个文件。

例子3的错误示范:

  1. funcmain(){// 文件1
  2. f,_:= os.Open("1.txt")if f !=nil{deferfunc(){if err := f.Close(); err !=nil{
  3. fmt.Printf("defer close file err 1 %v\n", err)}}()}// 文件2
  4. f,_= os.Open("2.txt")if f !=nil{deferfunc(){if err := f.Close(); err !=nil{
  5. fmt.Printf("defer close file err 2 %v\n", err)}}()}
  6. fmt.Println("success")}

执行结果:

  1. success
  2. deferclose file err 1close2.txt: file already closed

例子4:

  1. // 值传参funcfunc1(){var err errordefer fmt.Println(err)
  2. err = errors.New("func1 error")return}// 闭包funcfunc2(){var err errordeferfunc(){
  3. fmt.Println(err)}()
  4. err = errors.New("func2 error")return}// 值传参funcfunc3(){var err errordeferfunc(err error){
  5. fmt.Println(err)}(err)
  6. err = errors.New("func3 error")return}// 指针传参funcfunc4(){var err errordeferfunc(err *error){
  7. fmt.Println(*err)}(&err)
  8. err = errors.New("func4 error")return}funcmain(){func1()func2()func3()func4()}

执行结果:

  1. <nil>
  2. func2 error<nil>
  3. func4 error

解析:

第一个和第三个函数中,都是作为参数,进行值传参,

  1. err

在定义的时候就会求值,因为定义的时候值都是

  1. nil

,所以最后的结果都是

  1. nil

第二个函数的参数在定义的时候也求值了,但是它是个闭包,查看上下文发现最后值被修改为

  1. func2 error

第四个函数是指针传参,最后值被修改为

  1. func4 error

**现实中,第三个函数闭包的例子是比较容易犯的错误,导致最后

  1. defer

语句没有起到作用,造成生产上的事故,需要特别注意。**


三、defer的语句拆解

从返回值出发来拆解延迟语句

  1. defer

  1. return xxx

这条语句经过编译之后,实际上生成了三条指令:

1. 返回值 = xxx
2. 调用 defer 函数
3. 空的 return

其中,

  1. 1

  1. 3

  1. return

语句生成的指令,

  1. 2

  1. defer

语句生成的指令。可以看出:

**

  1. return

并不是一条原子指令;

  1. defer

语句在第二步调用,这里可能操作返回值,从而影响最终结果。**

接下来通过例子来加深理解。

例子1:

  1. funcfunc1()(r int){
  2. t :=3deferfunc(){
  3. t = t +3}()return t
  4. }funcmain(){
  5. r :=func1()
  6. fmt.Println(r)}

执行结果:

  1. 3

语句拆解:

  1. funcfunc1()(r int){
  2. t :=3// 1.返回值=xxx:赋值指令
  3. r = t
  4. // 2.调用defer函数:defer在赋值与返回之前执行,这个例子中返回值r没有被修改过func(){
  5. t = t +3}()// 3.空的returnreturn}funcmain(){
  6. r :=func1()
  7. fmt.Println(r)}

解析:因为第二个步骤里并没有操作返回值

  1. r

,所以最终得到的结果是

  1. 3

例子2:

  1. funcfunc2()(r int){deferfunc(r int){
  2. r = r +3}(r)return1}funcmain(){
  3. r :=func2()
  4. fmt.Println(r)}

执行结果:

  1. 1

语句拆解:

  1. funcfunc2()(r int){// 1.返回值=xxx:赋值指令
  2. r =1// 2.调用defer函数:因为是值传参,所以修改的r是个复制的值,不会影响要返回的那个r值。func(r int){
  3. r = r +3}(r)// 3.空的returnreturn}funcmain(){
  4. r :=func2()
  5. fmt.Println(r)}

解析:因为第二个步骤里改变的是传值进去的

  1. r

值,是一个形参的复制值,不会影响实参

  1. r

,所以最终得到的结果是

  1. 1

例子3:

  1. funcfunc3()(r int){deferfunc(){
  2. r = r +3}()return1}funcmain(){
  3. r :=func3()
  4. fmt.Println(r)}

执行结果:

  1. 4

语句拆解:

  1. funcfunc3()(r int){// 1.返回值=xxx:赋值指令
  2. r =1// 2.调用defer函数:因为是闭包,捕获的变量是引用传递,所以会修改返回的那个r值。func(){
  3. r = r +3}()// 3.空的returnreturn}funcmain(){
  4. r :=func3()
  5. fmt.Println(r)}

解析:因为第二个步骤里改变的

  1. r

值是闭包,闭包中捕获的变量是引用传递,不是值传递,所以最终得到的结果是

  1. 4


四、defer中的recover

代码中的

  1. panic

最终会被

  1. recover

捕获到。在日常开发中,可能某一条协议的逻辑触发了某一个

  1. bug

造成

  1. panic

,这时就可以用

  1. recover

去捕获

  1. panic

,稳住主流程,不影响其他协议的业务逻辑。

需要注意的是,**

  1. recover

函数只在

  1. defer

的函数中直接调用才生效。**

通过例子看

  1. recover

调用情况。

例子1:

  1. funcfunc1(){if err :=recover(); err !=nil{
  2. fmt.Println("func1 recover", err)return}}funcmain(){deferfunc1()panic("func1 panic")}

执行结果:

  1. func1 recover func1 panic

解析:正确

  1. recover

,因为在

  1. defer

中调用的,所以可以生效。

例子2:

  1. funcmain(){recover()panic("func2 panic")}

执行结果:

  1. panic: func2 panic
  2. goroutine 1[running]:
  3. main.main()
  4. C:/Users/ycz/go/ccc.go:5+0x31
  5. exit status 2

解析:错误

  1. recover

,直接调用

  1. recover

,返回

  1. nil

例子3:

  1. funcmain(){deferrecover()panic("func3 panic")}

执行结果:

  1. panic: func3 panic
  2. goroutine 1[running]:
  3. main.main()
  4. C:/Users/ycz/go/ccc.go:5+0x65
  5. exit status 2

解析:错误

  1. recover

  1. recover

需要在

  1. defer

的函数里调用。

例子4:

  1. funcmain(){deferfunc(){deferfunc(){recover()}()}()panic("func4 panic")}

执行结果:

  1. panic: func4 panic
  2. goroutine 1[running]:
  3. main.main()
  4. C:/Users/ycz/go/ccc.go:9+0x49
  5. exit status 2

解析:错误

  1. recover

,不能在多重

  1. defer

嵌套里调用

  1. recover

另外需要注意的一点是,**父

  1. goroutine

无法

  1. recover

住 子

  1. goroutine

  1. panic

。**

原因是,

  1. goroutine

被设计为一个独立的代码执行单元,拥有自己的执行栈,不与其他

  1. goroutine

共享任何的数据。

也就是说,无法让

  1. goroutine

拥有返回值,也无法让

  1. goroutine

拥有自身的

  1. ID

编号。

如果希望有一个全局的

  1. panic

捕获中心,那么可以通过

  1. channel

来实现,如下示例:

  1. var panicNotifyManage chaninterface{}funcStartGlobalPanicRecover(){
  2. panicNotifyManage =make(chaninterface{})gofunc(){select{case err :=<-panicNotifyManage:
  3. fmt.Println("panicNotifyManage--->", err)}}()}funcGoSafe(f func()){gofunc(){deferfunc(){if err :=recover(); err !=nil{
  4. panicNotifyManage <- err
  5. }}()f()}()}funcmain(){StartGlobalPanicRecover()
  6. f1 :=func(){panic("f1 panic")}GoSafe(f1)
  7. time.Sleep(time.Second)}

解析:

  1. GoSafe()

本质上是对

  1. go

关键字进行了一层封装,确保在执行并发单元前插入一个

  1. defer

,从而保证能够

  1. recover

  1. panic

。但是这个方案并不完美,如果开发人员不使用

  1. GoSafe

函数来创建

  1. goroutine

,而是自己创建,并且在代码中出现了

  1. panic

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


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

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

还没有评论