目录
panic和recover
作用
-
panic 能够改变程序的控制流,调用 panic 后会立刻停止执行当前函数的剩余代码,并在当前 Goroutine 中递归执行调用方的 defer;
-
recover 可以中止 panic 造成的程序崩溃。它是一个只能在 defer 中发挥作用的函数,在其他作用域中调用不会发挥作用;
注意事项
-
panic 只会触发当前 Goroutine 的 defer;
-
recover 只有在 defer 中调用才会生效;
-
panic 允许在 defer 中嵌套多次调用;
嵌套调用
Go 语言中的 panic 是可以多次嵌套调用的。一些熟悉 Go 语言的读者很可能也不知道这个知识点,如下所示的代码就展示了如何在 defer 函数中多次调用 panic:
func main() {
defer fmt.Println("in main")
defer func() {
defer func() {
panic("panic again and again")
}()
panic("panic again")
}()
panic("panic once")
}
$ go run main.go
in main
panic: panic once
panic: panic again
panic: panic again and again
goroutine 1 [running]:
...
exit status 2
从上述程序输出的结果,我们可以确定程序多次调用 panic 也不会影响 defer 函数的正常执行,所以使用 defer 进行收尾工作一般来说都是安全的。
数据结构
panic 关键字在 Go 语言的源代码是由数据结构 runtime._panic 表示的。每当我们调用 panic 都会创建一个如下所示的数据结构存储相关信息
type _panic struct {
argp unsafe.Pointer
arg interface{}
link *_panic
recovered bool
aborted bool
pc uintptr
sp unsafe.Pointer
goexit bool
}
-
argp 是指向 defer 调用时参数的指针;
-
arg 是调用 panic 时传入的参数;
-
link 指向了更早调用的 runtime._panic 结构;
-
recovered 表示当前 runtime._panic 是否被 recover 恢复;
-
aborted 表示当前的 panic 是否被强行终止;
从数据结构中的 link 字段我们就可以推测出以下的结论:panic 函数可以被连续多次调用,它们之间通过 link 可以组成链表。
defer
defer后边会接一个函数,但该函数不会立刻被执行,而是等到包含它的程序返回时(包含它的函数执行了return语句、运行到函数结尾自动返回、对应的goroutine panic)defer函数才会被执行。通常用于资源释放、打印日志、异常捕获等。
func main() {
f, err := os.Open(filename)
if err != nil {
return err
}
/**
* 这里defer要写在err判断的后边而不是os.Open后边
* 如果资源没有获取成功,就没有必要对资源执行释放操作
* 如果err不为nil而执行资源执行释放操作,有可能导致panic
*/
defer f.Close()
}
如果有多个defer函数,调用顺序类似于栈,越后面的defer函数越先被执行(后进先出)
func main() {
defer fmt.Println(1)
defer fmt.Println(2)
defer fmt.Println(3)
defer fmt.Println(4)
}
#输出结果:
4
3
2
1
defer影响返回值
如果包含defer函数的外层函数有返回值,而defer函数中可能会修改该返回值,最终导致外层函数实际的返回值可能与你想象的不一致,这里很容易踩坑,来几个例子:
例1
func f() (result int) {
defer func() {
result++
}()
return 0
}
例2
func f() (r int) {
t := 5
defer func() {
t = t + 5
}()
return t
}
例3
func f() (r int) {
defer func(r int) {
r = r + 5
}(r)
return 1
}
请先不要向下看,在心里跑一遍上边三个例子的结果,然后去验证
可能你会认为:例1的结果是0,例2的结果是10,例3的结果是6,那么很遗憾的告诉你,这三个结果都错了
为什么呢,最重要的一点就是要明白,return xxx这一条语句并不是一条原子指令
含有defer函数的外层函数,返回的过程是这样的:先给返回值赋值,然后调用defer函数,最后才是返回到更上一级调用函数中,可以用一个简单的转换规则将return xxx改写成
返回值 = xxx
调用defer函数(这里可能会有修改返回值的操作)
return 返回值
例1可以改写成这样
func f() (result int) {
result = 0
//在return之前,执行defer函数
func() {
result++
}()
return
}
所以例1的返回值是1
例2可以改写成这样
func f() (r int) {
t := 5
//赋值
r = t
//在return之前,执行defer函数,defer函数没有对返回值r进行修改,只是修改了变量t
func() {
t = t + 5
}
return
}
所以例2的结果是5
例3可以改写成这样
func f() (r int) {
//给返回值赋值
r = 1
/**
* 这里修改的r是函数形参的值,是外部传进来的
* func(r int){}里边r的作用域只该func内,修改该值不会改变func外的r值
*/
func(r int) {
r = r + 5
}(r)
return
}
所以例3的结果是1
defer函数传参
defer函数的参数值,是在申明defer时确定下来的
在defer函数申明时,对外部变量的引用是有两种方式:作为函数参数和作为闭包引用 作为函数参数,在defer申明时就把值传递给defer,并将值缓存起来,调用defer的时候使用缓存的值进行计算(如上边的例3) 而作为闭包引用,在defer函数执行时根据整个上下文确定当前的值
func main() {
i := 0
defer fmt.Println("a:", i)
//闭包调用,将外部i传到闭包中进行计算,不会改变i的值,如上边的例3
defer func(i int) {
fmt.Println("b:", i)
}(i)
//闭包调用,捕获同作用域下的i进行计算
defer func() {
fmt.Println("c:", i)
}()
i++
}
// 输出结果
c: 1
b: 0
a: 0
在defer中捕获panic消息
func main() {
r := doPanic()
println("Result:" + r.Message)
}
func doRecover(r *Result) {
if err := recover(); err != nil {
// 输出 panic 信息
fmt.Println(err)
// 打印调用栈
debug.PrintStack()
r.Message = err.(string)
}
println("->doRecover")
}
func doPanic() (r *Result) {
//err := errors.New("")
r = &Result{}
defer doRecover(r)
println("->doPanic")
panic("panic")
return r
}
type Result struct {
Message string
}
网友评论