前言
在 Go 语言中, panic
、 recover
和 defer
是三个用于处理异常和程序控制流的关键字, 这篇文章探讨它们的用法及场景,达到在写 Go 程序时可以更加灵活地进行错误处理和异常控制的目的。
1. panic
:运行时恐慌
无意引发 panic
看下面这段代码:
go
func main() {
s := []int{0, 1, 2, 3, 4}
a := s[5]
_ = a
}
这段代码,定义了一个切片,切片中有5个元素,索引为0~4,当我们访问索引为5的元素时,就会引发 panic
。
运行这段代码后,输出内容如下:
go
panic: runtime error: index out of range [5] with length 5
goroutine 1 [running]:
main.main()
/Users/GolandProjects/src/go-notes/main.go:5 +0x15
Process finished with the exit code 2
有意引发 panic
当程序调用 panic
函数时,当前函数的执行会停止,如果没有捕获该 panic
,程序将退出并输出相关的错误信息。
使用示例:
scss
func causePanic01() {
fmt.Println("执行函数causePanic01")
causePanic02()
fmt.Println("这行不会执行")
}
func causePanic02() {
fmt.Println("执行函数causePanic02")
panic("发生了不可恢复的错误")
fmt.Println("这行不会执行")
}
func main() {
fmt.Println("程序开始")
causePanic01()
fmt.Println("程序结束")
}
这段代码中,函数 causePanic02
, 调用 panic("发生了不可恢复的错误")
后,程序立即停止执行当前函数并进入恐慌状态, panic
后的代码不会被执行,程序会退出并打印堆栈跟踪。
执行后,输出内容如下:
less
程序开始
执行函数causePanic01
执行函数causePanic02
panic: 发生了不可恢复的错误
goroutine 1 [running]:
main.causePanic02()
/Users/GolandProjects/src/go-notes/main.go:13 +0x5f
main.causePanic01()
/Users/GolandProjects/src/go-notes/main.go:7 +0x55
main.main()
/Users/GolandProjects/src/go-notes/main.go:19 +0x55
Process finished with the exit code 2
从 panic
被引发到程序停止发生过程
看上面这段输出结果, main
函数调用了 causePanic01
函数,而 causePanic01
函数调用了 causePanic02
函数, causePanic02
函数中代码执行信息先出现,然后是 causePanic01
函数中代码的执行信息,最后才是 main
函数的信息。
总结一下:当引发 panic
时,初始的 panic
详情会被建立起来,程序控制权会立即从此行代码转移至调用其所属函数的那行 代码时,也就是调用栈中的上一级,一级一级沿着调用栈反方向找,直到我们编写的最外层函数那里,最后,程序崩溃停止运行, 终止前panic
详情被打印出来
2. defer
:延迟执行
defer
语句用于将一个函数调用推迟到当前函数的最后执行,无论当前函数是否正常返回, defer
后的函数都会执行。 它通常用于清理资源,如关闭文件、解锁互斥锁等。
使用案例
go
func deferExample() {
fmt.Println("开始执行函数")
// defer 语句的调用顺序是逆序执行的
defer fmt.Println("执行第一个 defer")
defer fmt.Println("执行第二个 defer")
fmt.Println("函数执行完毕")
}
func main() {
deferExample()
}
这段代码执行后,输出结果如下:
go
开始执行函数
函数执行完毕
执行第二个 defer
执行第一个 defer
为啥会这样输出呢?就是因为:
defer
语句的执行顺序是先进后出,即最后注册的defer
会最先执行。- 在函数
deferExample
中,defer
语句在函数结束时按照逆序执行。
3. recover
:恢复恐慌
recover
用于从恐慌中恢复,使程序继续执行。 recover
只能在 defer
函数中使用,并且只有当调用的 panic
发生时, recover
才会生效。如果 panic
没有发生, recover
返回 nil
。
为啥recover
只能在 defer
函数中使用?不可以单独使用吗?
下来看看下面这个案例:
go
func main() {
fmt.Println("程序开始")
panic("发生错误")
r := recover()
fmt.Printf("panic: %s\n", r)
fmt.Println("执行完成")
}
这段代码中,调用 panic
函数引发 panic
,然后调用 recover
函数恢复这个 panic
,运行后,程序依然崩溃, recover
函数 没有起到作用。原因通过上面的学习都知道, panic
一旦发生, panic
函数调用之后的代码都没有执行的机会。
正确使用 recover
+ defer
恢复恐慌
使用示例:
go
func safeCall() {
defer func() {
if r := recover(); r != nil {
fmt.Println("恢复了 panic:", r) // 捕获并恢复 panic
}
}()
fmt.Println("开始执行...")
panic("触发 panic") // 触发 panic
fmt.Println("这行不会执行")
}
func main() {
safeCall()
fmt.Println("程序继续执行")
}
执行这段代码,输出结果如下:
go
开始执行...
恢复了 panic: 触发 panic
程序继续执行
这段代码中,在 safeCall
函数中, defer
捕获到 panic
并调用 recover
,仅当调用结果不为 nil
时,才会打印。 这样,即使发生了 panic
,程序也能恢复并继续执行后续代码。
4. 一个函数怎样才能把 panic
转化为 error
类型值,并将其作为函数的结果值返回给调用方
这个问题我们就可以使用这三个关键字来实现:
go
func exampleFunction() (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("caught panic: %v", r)
}
}()
panic("something went wrong")
return nil // 这行不会执行,因为 panic 已经发生
}
func main() {
err := exampleFunction()
if err != nil {
fmt.Println("Function returned an error:", err)
} else {
fmt.Println("Function executed successfully")
}
}
这段代码,在 defer
中,使用 recover()
来捕获 panic
。如果 panic
被触发, recover()
会返回 panic
的参数值。 如果没有 panic
, recover()
会返回 nil
,这时函数会正常执行并返回 nil
。
recover()
捕获到的 panic
通常是一个 interface{}
类型,可以是任何类型。你可以将它转化为一个 error 类型, 在这里,我们使用 fmt.Errorf("caught panic: %v", r)
来将 panic
的内容转化为 error
类型。
最后
这些三个关键字通常一起使用,在处理程序异常、错误恢复和资源清理时非常有用。