开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 7天,点击查看活动详情
一块来学习一下Go的defer、panic和recover的常规用法,以及深度解析容易掉入的陷阱,看看如何规避。
defer
Go语言的defer
语句会将其后面跟随的语句进行延迟处理。在defer
归属的函数即将返回时,将延迟处理的语句按defer
定义的逆序进行执行(类似数据结构栈
,先进后出)
用途:非常适合用于处理资源释放问题,比如:资源清理、文件关闭、解锁及记录时间等。
简单示例
func main() {
fmt.Println("start")
defer fmt.Println(111)
defer fmt.Println(222)
defer fmt.Println(333)
fmt.Println("end")
//输出:
//start
//end
//333
//222
//111
}
执行时机
Go语言的函数中return
语句在底层并不是原子操作,它分为给返回值赋值
和RET指令
两步。而defer
语句执行的时机就在返回值赋值操作后,RET指令执行前。如下图所示:
使用一个经典案例,加深下执行时机的理解(思考下结果为什么是5 6 5 5?):
func main() {
fmt.Println(f1()) //5
fmt.Println(f2()) //6
fmt.Println(f3()) //5
fmt.Println(f4()) //5
}
func f1() int {
x := 5
defer func() {
x++
}()
return x
}
func f2() (x int) {
defer func() {
x++
}()
return 5
}
func f3() (y int) {
x := 5
defer func() {
x++
}()
return x
}
func f4() (x int) {
defer func(x int) {
x++
}(x)
return 5
}
思考过程解析:
f1()
: 执行retrun x
,先做返回值赋值为5,然后defer里x++
,这时并不会重新给返回值赋值,所以结果是5;f2()
: 执行retrun 5
,先做返回值赋值为5,因为x为返回值命名写法,所以defer里x++
,会重新给返回值赋值,所以结果为6;f3()
: 执行retrun x
,先做返回值赋值为5,然后x做值拷贝给y,后来的defer里x++
,不会影响y的值,所以结果为5;f4()
: 执行retrun 5
,先做返回值赋值为5,然后虽然x为返回值命名写法,但defer里重新声明了x为局部变量做x++
,这样的操作不会给返回值重新赋值,所以结果为5。
陷阱
先来看看两个例子,输出什么结果,然后再进一步做陷阱分析
例子1:看看结果输出什么?
func main() {
x := 1
y := 2
defer calcTest("AA", x, calcTest("A", x, y))
x = 10
defer calcTest("BB", x, calcTest("B", x, y))
y = 20
}
func calcTest(index string, a, b int) int {
ret := a + b
fmt.Println(index, a, b, ret)
return ret
}
例子2:上面例子中的defer语句改个写法(使用func(){...}()
做嵌套),看看结果输出什么?
func main() {
x := 1
y := 2
defer func() {
calcTest("AA", x, calcTest("A", x, y))
}()
x = 10
defer func() {
calcTest("BB", x, calcTest("B", x, y))
}()
y = 20
}
两个例子,差异就在于使不使用func(){...}()
做嵌套,为什么结果却截然不同呢?
例子1输出结果:
A 1 2 3
B 10 2 12
BB 10 12 22
AA 1 3 4
例子2输出结果:
B 10 20 30
BB 10 30 40
A 10 20 30
AA 10 30 40
陷阱分析:
- 首先,要先记住
defer
语句后面,如果未使用嵌套写法,这时函数里的参数值都得为确定值,即不会因为后面的修改而影响;如果使用嵌套写法,则不是确定值,即后面修改后会同步生效; - 所以,例子1执行顺序:先执行输出A→B,然后defer逆序执行输出BB→AA;例子2执行顺序:直接defer逆序执行,顺序为B→BB→A→AA,每一步带入相应的x y值,即可推导出结果。
panic和recover
Go应用程序执行时遇到panic
,是会导致程序崩溃,异常退出,panic
之后的代码不会被执行。
简单示例
func main() {
AAA()
BBB()
CCC()
}
func AAA() {
fmt.Println("func AAA")
}
func BBB() {
panic("func BBB")
}
func CCC() {
fmt.Println("func CCC")
}
//输出结果:
//func AAA
//panic: func BBB
//
//goroutine 1 [running]:
//main.BBB(...)
// .../09_func_panic_recover.go:27
//main.main()
// .../09_func_panic_recover.go:7 +0x96
//exit status 2
为了避免这个“程序崩溃,异常退出”问题,则需要使用recover
函数来处理。
func main() {
AAA()
BBB()
CCC()
}
func AAA() {
fmt.Println("func AAA")
}
func BBB() {
defer func() {
if err := recover(); err != nil {
fmt.Println("recover in func BBB")
fmt.Println(fmt.Sprintf("%T %v", err, err))
//...handle 打日志等
}
}()
panic("func BBB") //panic之前,如果没有recover,则程序会崩溃,异常退出。有则继续执行
}
func CCC() {
fmt.Println("func CCC")
}
//输出结果:
//func AAA
//recover in func BBB
//string func BBB
//func CCC
注意点
recover
必须搭配defer
来使用,否则panic
捕获不到;defer
一定要在可能引发panic
的语句之前定义;
陷阱
如果想把recover处理逻辑抽象出来作为通用函数,则在defer
语句后面,直接调用函数,切记不要使用 func(){...}()
去嵌套调用,因为这样会导致捕捉不到panic
。示例如下:
func main() {
//defer DefaultRecover() //能捕获panic
defer func() { //不能捕获panic
DefaultRecover()
}()
panic("func main")
}
func DefaultRecover() {
if err := recover(); err != nil {
fmt.Println("recover in func DefaultRecover")
fmt.Println(fmt.Sprintf("%T %v", err, err))
//...handle 打日志等
}
}
//输出结果:
//panic: func DDD
//goroutine 1 [running]:
//main.DDD()
// .../09_func_panic_recover.go:46 +0x5b
//main.main()
// .../09_func_panic_recover.go:10 +0xdc
//exit status 2
如果本文对你有帮助,欢迎点赞收藏加关注,如果本文有错误的地方,欢迎指出!