脚本之家,脚本语言编程技术及教程分享平台!
分类导航

Python|VBS|Ruby|Lua|perl|VBA|Golang|PowerShell|Erlang|autoit|Dos|bat|

服务器之家 - 脚本之家 - Golang - Go基础教程系列之defer、panic和recover详解

Go基础教程系列之defer、panic和recover详解

2022-09-22 13:38骏马金龙 Golang

这篇文章主要介绍了Go基础教程系列之defer、panic和recover,需要的朋友可以参考下

defer关键字

defer关键字可以让函数或语句延迟到函数语句块的最结尾时,即即将退出函数时执行,即便函数中途报错结束、即便已经panic()、即便函数已经return了,也都会执行defer所推迟的对象。

其实defer的本质是,当在某个函数中使用了defer关键字,则创建一个独立的defer栈帧,并将该defer语句压入栈中,同时将其使用的相关变量也拷贝到该栈帧中(显然是按值拷贝的)。因为栈是LIFO方式,所以先压栈的后执行。因为是独立的栈帧,所以即使调用者函数已经返回或报错,也一样能在它们之后进入defer栈帧去执行。

例如:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func main() {
    a()
}
 
func a() {
    println("in a")
    defer b()              // 将b()压入defer栈中
    println("leaving a")
    //到了这里才会执行b()
}
 
func b() {
    println("in b")
    println("leaving b")
}

上面将输出:

?
1
2
3
4
in a
leaving a
in b
leaving b

即便是函数已经报错,或函数已经return返回,defer的对象也会在函数退出前的最后一刻执行。

?
1
2
3
4
5
6
7
8
9
10
11
12
func a() TYPE{
    ...CODE...
    
    defer b()
    
    ...CODE...
    
    // 函数执行出了错误
    
    return args
    // 函数b()都会在这里执行
}

但注意,由于Go的作用域采用的是词法作用域,defer的定义位置决定了它推迟对象能看见的变量值,而不是推迟对象被调用时所能看见的值。

例如:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main
 
var x = 10
func main() {
    a()
}
 
func a() {
    println("start a:",x)   // 输出10
    x = 20
    defer b(x)       // 压栈,并按值拷贝20到栈中
    x = 30
    println("leaving a:",x)  // 输出30
    // 调用defer延迟的对象b(),输出20
}
 
func b(x int) {
    println("start b:",x)
}

比较下面的defer:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main
 
var x = 10
 
func main() {
    a()
}
 
func a() int {
    println("start a:", x) // 输出10
    x = 20
    defer func() {      // 压栈,但并未传值,所以内部引用x
        println("in defer:", x)  // 输出30
    }()
    x = 30
    println("leaving a:", x) // 输出30
    return x
}

上面defer推迟的匿名函数输出的值是30,它看见的不应该是20吗?先再改成下面的:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main
 
var x = 10
 
func main() {
    a()
}
 
func a() int {
    println("start a:", x) // 输出10
    x = 20
    defer func(x int) {
        println("in defer:", x)  // 输出20
    }(x)
    x = 30
    println("leaving a:", x) // 输出30
    return x
}

这个defer推迟的对象中看见的却是20,这和第一种defer b(x)是相同的。

原因在于defer推迟的如果是函数,它直接就在它的定义位置处评估好参数、变量。该拷贝传值的拷贝传值,该指针相见的指针相见。所以,对于第(1)和第(3)种情况,在defer的定义位置处,就将x=20拷贝给了推迟的函数参数,所以函数内部操作的一直是x的副本。而第二种情况则是直接指向它所看见的x=20那个变量,则个变量是全局变量,当执行x=30的时候会将其值修改,到执行defer推迟的对象时,它指向的x的值已经是修改过的。

再看下面这个例子,将defer放进一个语句块中,并在这个语句块中新声明一个同名变量x:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
func a() int {
    println("start a:", x) // 输出10
    x = 20
    {
        x := 40
        defer func() {
            println("in defer:", x)  // 输出40
        }()
    }
    x = 30
    println("leaving a:", x) // 输出30
    return x
}

上面的defer定义在语句块中,它能看见的x是语句块中x=40,它的x指向的是语句块中的x。另一方面,当语句块结束时,x=40的x会消失,但由于defer的函数中仍有x指向40这个值,所以40这个值仍被defer的函数引用着,它直到defer执行完之后才会被GC回收。所以defer的函数在执行的时候,仍然会输出40。

如果语句块内有多个defer,则defer的对象以LIFO(last in first out)的方式执行,也就是说,先定义的defer后执行。

?
1
2
3
4
5
6
7
8
func main() {
    println("start...")
    defer println("1")
    defer println("2")
    defer println("3")
    defer println("4")
    println("end...")
}

将输出:

?
1
2
3
4
5
6
start...
end...
4
3
2
1

defer有什么用呢?一般用来做善后操作,例如清理垃圾、释放资源,无论是否报错都执行defer对象。另一方面,defer可以让这些善后操作的语句和开始语句放在一起,无论在可读性上还是安全性上都很有改善,毕竟写完开始语句就可以直接写defer语句,永远也不会忘记关闭、善后等操作。

例如,打开文件,关闭文件的操作写在一起:

?
1
2
3
open()
defer file.Close()
... 操作文件 ...

以下是defer的一些常用场景:

  • 打开关闭文件
  • 锁定、释放锁
  • 建立连接、释放连接
  • 作为结尾输出结尾信息
  • 清理垃圾(如临时文件)

panic()和recover()

panic()用于产生错误信息并终止当前的goroutine,一般将其看作是退出panic()所在函数以及退出调用panic()所在函数的函数。例如,G()中调用F(),F()中调用panic(),则F()退出,G()也退出。

注意,defer关键字推迟的对象是函数最后调用的,即使出现了panic也会调用defer推迟的对象。

例如,下面的代码中,main()中输出一个start main之后调用a(),它会输出start a,然后就panic了,panic()会输出panic: panic in a,然后报错,终止程序。

?
1
2
3
4
5
6
7
8
9
10
11
func main() {
    println("start main")
    a()
    println("end main")
}
 
func a() {
    println("start a")
    panic("panic in a")
    println("end a")
}

执行结果如下:

?
1
2
3
4
5
6
7
8
9
10
start main
start a
panic: panic in a
 
goroutine 1 [running]:
main.a()
        E:/learning/err.go:14 +0x63
main.main()
        E:/learning/err.go:8 +0x4c
exit status 2

注意上面的end aend main都没有被输出。

可以使用recover()去捕获panic()并恢复执行。recover()用于捕捉panic()错误,并返回这个错误信息。但注意,即使recover()捕获到了panic(),但调用含有panic()函数的函数(即上面的G()函数)也会退出,所以如果recover()定义在G()中,则G()中调用F()函数之后的代码都不会执行(见下面的通用格式)。

以下是比较通用的panic()和recover()的格式:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func main() {
    G()
    // 下面的代码会执行
    ...CODE IN MAIN...
}
func G(){
    defer func (){
        if str := recover(); str != nil {
            fmt.Println(str)
        }
    }()
    ...CODE IN G()...
    
    // F()的调用必须在defer关键字之后
    F()
    // 该函数内下面的代码不会执行
    ...CODE IN G()...
}
func F() {
    ...CODE1...
    panic("error found")
    // 下面的代码不会执行
    ...CODE IN F()...
}

可以使用recover()去捕获panic()并恢复执行。但以下代码是错误的:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func main() {
    println("start main")
    a()
    println("end main")
}
 
func a() {
    println("start a")
    panic("panic in a")
 
    // 直接放在panic后是错误的
    panic_str := recover()
    println(panic_str)
 
    println("end a")
}

之所以错误,是因为panic()一出现就直接退出函数a()和main()了。要想recover()真正捕获panic(),需要将recover()放在defer的推迟对象中,且defer的定义必须在panic()发生之前。

例如,下面是通用格式的示例:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package main
 
import "fmt"
 
func main() {
    println("start main")
    b()
    println("end main")
}
 
func a() {
    println("start a")
    panic("panic in a")
    println("end a")
}
 
func b() {
    println("start b")
    defer func() {
        if str := recover(); str != nil {
            fmt.Println(str)
        }
    }()
    a()
    println("end b")
}

以下是输出结果:

?
1
2
3
4
5
start main
start b
start a
panic in a
end main

注意上面的end bend a都没有被输出,但是end main输出了。

panic()是内置的函数(在包builtin中),在log包中也有一个Panic()函数,它调用Print()输出信息后,再调用panic()。go doc log Panic一看便知:

?
1
2
3
$ go doc log Panic
func Panic(v ...interface{})
    Panic is equivalent to Print() followed by a call to panic().

更多关于 Go基础教程系列之defer、panic和recover详解 请查看下面的相关链接

原文链接:https://www.cnblogs.com/f-ck-need-u/p/9879198.html

延伸 · 阅读

精彩推荐
  • GolangGo 代码生成工具详解

    Go 代码生成工具详解

    这篇文章主要介绍了Go 代码生成工具详解,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下...

    新亮笔记4912021-04-18
  • GolangGo 语言选择器实例教程

    Go 语言选择器实例教程

    这篇文章主要为大家介绍了Go 语言选择器实例教程,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪...

    cureking8022022-07-14
  • GolangGo中Channel发送和接收操作指南

    Go中Channel发送和接收操作指南

    在golang中channel属于较为核心的一个功能,尤其在go协程中,channel功能尤为重要,下面这篇文章主要给大家介绍了关于Go中Channel发送和接收操作的相关资料,需要...

    呦呦鹿鸣11112021-09-16
  • Golang浅谈beego默认处理静态文件性能低下的问题

    浅谈beego默认处理静态文件性能低下的问题

    下面小编就为大家带来一篇浅谈beego默认处理静态文件性能低下的问题。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看...

    jingxian5722020-05-07
  • GolangGolang中的变量学习小结

    Golang中的变量学习小结

    本文主要带大家学习了Golang里面的四大类型的变量,十分的详细,有需要的小伙伴可以参考下 ...

    _AlphaBaby_3242020-05-20
  • Golang完美解决golang go get私有仓库的问题

    完美解决golang go get私有仓库的问题

    这篇文章主要介绍了完美解决golang go get私有仓库的问题,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...

    smokelee10282021-06-19
  • Golang解决golang结构体tag编译错误的问题

    解决golang结构体tag编译错误的问题

    这篇文章主要介绍了解决golang结构体tag编译错误的问题,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...

    songzhaorong11872021-06-14
  • GolangGolang的循环语句和循环控制语句详解

    Golang的循环语句和循环控制语句详解

    循环语句为了简化程序中有规律的重复性操作,需要用到循环语句,和其他大多数编程语言一样,GO的循环语句有for循环,不同的是没有while循环,而循环控...

    曲鸟9012021-12-02