在Golang的异常处理体系中,panic是用于表示不可恢复错误的机制,它的触发主要分为运行时自动触发和开发者手动触发两类,不同触发场景对应不同的使用场景和处理逻辑。

一、运行时错误自动触发panic
Golang运行时会在程序出现无法继续执行的错误时自动触发panic,这类panic不需要开发者主动调用相关函数,是运行时系统自动检测的。常见的运行时错误触发场景包括以下几种:
1. 数组或切片越界访问
当访问数组或切片的索引超出其有效范围时,运行时会直接触发panic。
package main
import "fmt"
func main() {
arr := []int{1, 2, 3}
// 访问索引3,超出arr的长度3(有效索引为0-2),触发运行时panic
fmt.Println(arr[3])
}
2. 空指针解引用
当尝试对值为nil的指针进行解引用操作时,运行时会触发panic。
package main
import "fmt"
func main() {
var p *int
// p为nil指针,解引用触发panic
fmt.Println(*p)
}
3. 除数为0的整数除法
Golang中整数类型的除法如果除数为0,会触发运行时panic,浮点数除数为0不会触发panic,会得到无穷大或NaN的结果。
package main
import "fmt"
func main() {
a := 10
b := 0
// 整数除以0,触发运行时panic
fmt.Println(a / b)
}
4. 关闭已关闭的通道
对已经处于关闭状态的通道执行关闭操作,会触发运行时panic。
package main
func main() {
ch := make(chan int, 1)
close(ch)
// 再次关闭已关闭的通道,触发panic
close(ch)
}
二、手动触发panic
除了运行时自动触发的panic,开发者还可以通过内置的panic()函数主动抛出panic,通常用于处理业务逻辑中不可恢复的错误场景。
1. 手动抛出panic的基本用法
panic()函数可以接收任意类型的参数作为panic的信息,通常我们会传入字符串描述错误原因。
package main
import "fmt"
func divide(a, b int) int {
if b == 0 {
// 手动触发panic,提示除数不能为0
panic("除数不能为0")
}
return a / b
}
func main() {
result := divide(10, 0)
fmt.Println(result)
}
2. 手动panic的参数类型
panic()的参数可以是任意类型,实际开发中也可以传入自定义的结构体来携带更详细的错误信息。
package main
import "fmt"
type MyError struct {
Code int
Message string
}
func process() {
// 传入自定义结构体作为panic的参数
panic(MyError{Code: 500, Message: "处理过程发生内部错误"})
}
func main() {
process()
}
三、panic的执行流程与recover捕获
无论是运行时触发还是手动抛出的panic,一旦触发就会终止当前函数的执行,然后按照调用栈向上逐层执行defer语句,直到被recover捕获或者程序终止。
1. panic触发后的执行顺序
panic触发后,当前函数中panic之后的代码不会执行,会先执行当前函数中已经定义的defer语句,再返回到上层调用函数继续执行上层函数的defer,以此类推。
package main
import "fmt"
func funcA() {
fmt.Println("funcA开始执行")
funcB()
fmt.Println("funcA执行结束") // panic触发后不会执行
}
func funcB() {
defer fmt.Println("funcB的defer执行")
fmt.Println("funcB开始执行")
panic("funcB中触发panic")
fmt.Println("funcB执行结束") // 不会执行
}
func main() {
funcA()
}
上述代码的执行结果会先输出funcA开始执行、funcB开始执行、funcB的defer执行,然后抛出panic信息,funcA中panic之后的代码不会执行。
2. 使用recover捕获panic
recover是Golang内置的用于捕获panic的函数,它只能在defer语句中生效,捕获到panic后会返回panic传入的参数,程序可以继续执行,不会崩溃。
package main
import "fmt"
func divide(a, b int) (result int, err interface{}) {
defer func() {
// 在defer中使用recover捕获panic
if r := recover(); r != nil {
err = r
}
}()
if b == 0 {
panic("除数不能为0")
}
result = a / b
return
}
func main() {
res, err := divide(10, 0)
if err != nil {
fmt.Printf("捕获到panic:%vn", err)
} else {
fmt.Printf("计算结果:%dn", res)
}
}
四、panic使用的注意事项
- panic适合用于处理不可恢复的错误,比如程序启动时的配置文件缺失、关键依赖不可用等场景,普通的业务错误建议使用error返回值处理。
- recover必须放在defer语句中才能生效,直接调用recover无法捕获panic。
- 不要在for循环或者频繁调用的函数中随意使用panic,频繁的panic和recover会带来额外的性能开销。
- 手动触发panic时,建议传入清晰的错误信息,方便后续排查问题。
| 触发类型 | 触发场景 | 处理方式 |
|---|---|---|
| 运行时自动触发 | 数组越界、空指针解引用、整数除0、重复关闭通道等 | 提前做好边界校验、空值判断避免触发,或通过recover捕获 |
| 手动触发 | 业务逻辑中不可恢复的错误 | 合理判断触发条件,配合recover使用避免程序崩溃 |