defer关键字、panic和recover的示例分析

65次阅读
没有评论

共计 4198 个字符,预计需要花费 11 分钟才能阅读完成。

这篇文章给大家介绍 defer 关键字、panic 和 recover 的示例分析,内容非常详细,感兴趣的小伙伴们可以参考借鉴,希望对大家能有所帮助。

defer 关键字

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

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

例如:

func main() {    a()} func a() { println( in a) defer b() // 将 b()压入 defer 栈中 println(leaving a) // 到了这里才会执行 b()} func b() { println( in b) println(leaving b)}

上面将输出:

in aleaving a in bleaving b

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

func a() TYPE{    …CODE… defer b()        …CODE… // 函数执行出了错误 return args // 函数 b()都会在这里执行}

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

例如:

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:

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 吗?先再改成下面的:

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:

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 后执行。

func main() { println( start…) defer println(1) defer println(2) defer println(3) defer println(4) println(end…)}

将输出:

start… end… 4 3 2 1

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

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

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,然后报错,终止程序。

func main() { println( start main)    a() println( end main)} func a() { println( start a) panic(panic in a) println(end a)}

执行结果如下:

start mainstart apanic: panic in agoroutine 1 [running]:main.a()        E:/learning/err.go:14 +0x63main.main()        E:/learning/err.go:8 +0x4c exit status 2

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

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

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

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()并恢复执行。但以下代码是错误的:

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()发生之前。

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

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)}

以下是输出结果:

start main start b start apanic in a end main

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

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

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

关于 defer 关键字、panic 和 recover 的示例分析就分享到这里了,希望以上内容可以对大家有一定的帮助,可以学到更多知识。如果觉得文章不错,可以把它分享出去让更多的人看到。

正文完
 
丸趣
版权声明:本站原创文章,由 丸趣 2023-07-18发表,共计4198字。
转载说明:除特殊说明外本站除技术相关以外文章皆由网络搜集发布,转载请注明出处。
评论(没有评论)