在 Go 语言中,有很多流程控制的语句,if、else 等等,这些流程控制的关键字在其他语言中也存在的但 Go 中还有几个特殊的流程控制关键字,defer、panic 和 recover。
1. defer
defer 可以保证一些代码在函数或者方法返回之前被调用,即使方法没有正常执行完,发生了 panic,defer 后面的代码也会执行。这里需要注意,不是在退出某个作用域之前会被调用,而且函数或者方法。
defer 通常用来回收一些资源,比如关闭文件,关闭数据库连接以及释放一些资源(释放锁)。在使用 defer 的时候,有一些需要注意的地方。
- 参数预计算
在定义 defer 的时候,引用的外部参数会立刻被拷贝,在 i++ 执行之前就已经确定了,下面的代码最后打印的值是 0, 而不是1:
func t1() {
i := 0
defer fmt.Println(i)
i++
}
如果想要最后打印的值是 1,则做如下修改:
func t2() {
i := 0
defer func() {
fmt.Println(i)
}()
i++
}
- 如果定义了多个 defer 语句,最后定义的最先执行
下面的代码输出的结果是 4 3 2 1:
func t3() {
defer fmt.Println(1)
defer fmt.Println(2)
defer fmt.Println(3)
defer fmt.Println(4)
}
- defer 可以对方法或者函数的命名返回值进行赋值
在下面的代码中,正常情况下回返回 1,使用 defer 却可以对返回值继续赋值,所以最后的返回值是 2。
2. panic
panic 是 Go 的内置函数,可以打断当前的代码的正常执行流程,如果一个函数中出现 panic,该函数后续的代码都会停止执行。但是会执行 F 中的 defer 代码。然后其他调用 F 函数的地方也会出现 panic,层层向上传递,直到栈顶,最后程序会崩溃。
panic 可以显示调用 panic 函数产生,也会通过一些运行时的错误产生,比如数组越界。
在网上有很多文章会说,panic 只会调用当前 goroutine 的 defer 代码,其实这种说法是不正确的,比如下面的代码:
func main() {
go func() {
defer fmt.Println("goroutine1 invoke")
go func() {
defer fmt.Println("goroutine2 invoke")
go func() {
defer fmt.Println("goroutine3 invoke")
panic("panic")
}()
}()
}()
time.Sleep(1 * time.Second)
}
很大概率会输出下面的结果:
goroutine1 invoke
goroutine2 invoke
goroutine3 invoke
panic: panic
准确的说法是 panic 只会保证当前 goroutine 中的 defer 代码一定会执行,其他 goroutine 中的 defer 代码不保证能执行。
3. recover
recover 也是 Go 的内置函数,这个函数可以从 panic 中恢复程序的正常执行。recover 需要和 defer 定义在一起。
在正常的流程中,recover 的执行不会产生任何影响。只有在 panic 发生的时候,recevoer 才会恢复应用,阻止程序崩溃。而 panic 发生的时候只会执行 defer 代码。所以 recover 只在和 defer 搭配的时候才会有意义。
recover 和 panic 需要在同一个 goroutine 使用,跨 goroutine 无法恢复应用。
go func() {
defer fmt.Println("goroutine1 invoke")
go func() {
defer fmt.Println("goroutine2 invoke")
go func() {
defer func() {
recover()
}()
defer fmt.Println("goroutine3 invoke")
panic("panic")
}()
}()
}()
time.Sleep(1 * time.Second)
下面的程序不会出现崩溃,但如果对 recover 的调用不在同一个 goroutine 中,就无法阻止程序的崩溃。
go func() {
defer fmt.Println("goroutine1 invoke")
go func() {
defer func() {
recover()
}()
defer fmt.Println("goroutine2 invoke")
go func() {
defer fmt.Println("goroutine3 invoke")
panic("panic")
}()
}()
}()
time.Sleep(1 * time.Second)
4. 小结
defer、panic、recover 是 Go 提供的流程控制方式,defer 可以用于正常的代码流程,用于关闭资源等操作,panic 则用来表示程序出现大问题,需要终止,可以自行触发,也可以被一些运行时的错误触发。但在一些情况下,我们不希望程序因为 panic 而终止,比如 web 服务,可以通过 recoever 来恢复程序。
本文基于 go1.14
文 / Rayjun
网友评论