字数 2014,阅读大约需 11 分钟
资料
Go 语言 panic 和 recover 的原理 | Go 语言设计与实现[1]
panic与recover机制 | 深入Go语言之旅[2]
说明
•
panic
能够改变程序的控制流,调用panic
后会立刻停止执行当前函数的剩余代码,并在当前 Goroutine 中递归执行调用方的defer
;•
recover
可以中止panic
造成的程序崩溃。它是一个只能在defer
中发挥作用的函数,在其他作用域中调用不会发挥作用;•
panic
只会触发当前 Goroutine 的defer
;•
recover
只有在defer
中调用才会生效;•
panic
允许在defer
中嵌套多次调用;
func main() {
defer fmt.Println("in main")
if err := recover(); err != nil {
fmt.Println(err)
}
panic("unknown err")
}
注意
“多个延迟函数,会组成一个链表。Go在发生panic过程中,会依次遍历该链表,并检查链表中的延迟函数是否调用了 recover
函数调用,若调用了则 panic
异常会被捕获而不会继续向上抛出,否则会继续向上抛出异常和执行延迟函数,直到该 panic
没有被捕获,进程异常终止,这个过程叫做panicking。我们需要知道的是即使panic被延迟函数链表中某个延迟函数捕获处理了,但其他的延迟函数还是会继续执行的,只是panic异常不在继续抛出。”
使用
Go语言的panic和recover是处理程序异常和错误恢复的机制。让我详细解释它们的工作原理和使用方式:
1. panic的本质和触发条件
panic是什么
panic是Go语言中的一种异常机制,当程序遇到无法处理的错误时会触发panic,导致程序停止正常执行。
常见的panic触发场景
// 1. 数组/切片越界
func arrayPanic() {
arr := [3]int{1, 2, 3}
fmt.Println(arr[5]) // panic: runtime error: index out of range [5] with length 3
}
// 2. nil指针解引用
func nilPointerPanic() {
var p *int
fmt.Println(*p) // panic: runtime error: invalid memory address or nil pointer dereference
}
// 3. 向nil map写入
func nilMapPanic() {
var m map[string]int
m["key"] = 1 // panic: assignment to entry in nil map
}
// 4. 关闭已关闭的通道
func channelPanic() {
ch := make(chan int)
close(ch)
close(ch) // panic: close of closed channel
}
// 5. 类型断言失败
func typeAssertionPanic() {
var i interface{} = "hello"
num := i.(int) // panic: interface conversion: interface {} is string, not int
}
// 6. 手动触发panic
func manualPanic() {
panic("something went wrong") // 手动触发panic
}
2. panic的执行流程
func main() {
fmt.Println("开始执行")
func1()
fmt.Println("这行不会执行") // panic后不会到达这里
}
func func1() {
defer fmt.Println("func1 的 defer 会执行")
func2()
fmt.Println("这行不会执行")
}
func func2() {
defer fmt.Println("func2 的 defer 会执行")
panic("发生了panic")
fmt.Println("这行不会执行")
}
// 输出:
// 开始执行
// func2 的 defer 会执行
// func1 的 defer 会执行
// panic: 发生了panic
panic的执行顺序:
1. 停止当前函数的正常执行
2. 执行当前函数的所有defer语句(按LIFO顺序)
3. 返回到调用函数,重复步骤2
4. 一直向上传播,直到main函数
5. 程序崩溃并打印panic信息
3. recover的使用
recover只能在defer函数中调用,用于捕获和处理panic。
// ✅ 基本的recover使用
func safeFunction() {
defer func() {
if r := recover(); r != nil {
fmt.Printf("捕获到panic: %v\n", r)
}
}()
panic("测试panic")
fmt.Println("这行不会执行")
}
// ✅ 更完整的错误处理
func robustFunction() (err error) {
defer func() {
if r := recover(); r != nil {
// 将panic转换为error
err = fmt.Errorf("panic recovered: %v", r)
// 记录错误日志
log.Printf("panic occurred: %v", r)
// 可以添加调用栈信息
debug.PrintStack()
}
}()
// 可能发生panic的代码
riskyOperation()
return nil
}
func riskyOperation() {
panic("something bad happened")
}
4. 实际应用场景和最佳实践
4.1 Web服务器的panic恢复
// ✅ HTTP处理器的panic恢复中间件
func panicRecoveryMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
// 记录错误
log.Printf("Panic in handler: %v\n%s", err, debug.Stack())
// 返回500错误而不是让服务器崩溃
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}
// 使用示例
func main() {
handler := panicRecoveryMiddleware(http.HandlerFunc(riskyHandler))
http.ListenAndServe(":8080", handler)
}
func riskyHandler(w http.ResponseWriter, r *http.Request) {
// 这里可能发生panic的代码
var users []User
fmt.Fprintf(w, "User: %s", users[0].Name) // 如果users为空会panic
}
4.2 协程池的panic处理
// ✅ 工作协程的panic恢复
func worker(id int, jobs <-chan Job, results chan<- Result) {
for job := range jobs {
func() {
defer func() {
if r := recover(); r != nil {
log.Printf("Worker %d panic: %v\n%s", id, r, debug.Stack())
// 发送错误结果而不是让整个程序崩溃
results <- Result{Error: fmt.Errorf("worker panic: %v", r)}
}
}()
// 处理任务
result := processJob(job)
results <- result
}()
}
}
type Job struct {
ID int
Data string
}
type Result struct {
JobID int
Data string
Error error
}
4.3 资源清理
// ✅ 确保资源清理即使在panic时也能执行
func processFile(filename string) (err error) {
file, err := os.Open(filename)
if err != nil {
return err
}
defer func() {
// 确保文件总是被关闭
if closeErr := file.Close(); closeErr != nil && err == nil {
err = closeErr
}
// 处理可能的panic
if r := recover(); r != nil {
err = fmt.Errorf("panic during file processing: %v", r)
}
}()
// 可能panic的文件处理逻辑
return processFileContent(file)
}
5. 何时使用panic vs error
✅ 应该使用panic的场景:
1. 程序逻辑错误(编程错误,不应该发生)
2. 初始化失败(程序无法继续运行)
3. 不可恢复的系统错误
// ✅ 适合使用panic的场景
func NewDatabase(connectionString string) *Database {
db, err := sql.Open("postgres", connectionString)
if err != nil {
// 数据库连接失败,程序无法继续
panic(fmt.Sprintf("Failed to connect to database: %v", err))
}
return &Database{db: db}
}
// ✅ 检查程序逻辑错误
func divide(a, b int) int {
if b == 0 {
panic("division by zero") // 这是程序逻辑错误
}
return a / b
}
✅ 应该使用error的场景:
1. 预期可能发生的错误
2. 用户输入错误
3. 网络/IO错误
4. 业务逻辑错误
// ✅ 适合返回error的场景
func ValidateUser(user User) error {
if user.Name == "" {
return errors.New("username is required")
}
if user.Age < 0 {
return errors.New("age must be positive")
}
return nil
}
func FetchUserFromAPI(id int) (*User, error) {
resp, err := http.Get(fmt.Sprintf("/api/users/%d", id))
if err != nil {
return nil, fmt.Errorf("failed to fetch user: %w", err)
}
// ... 处理响应
return user, nil
}
6. 常见的panic/recover反模式
❌ 不要滥用panic作为控制流
// ❌ 错误:用panic作为控制流
func findUser(users []User, id int) User {
for _, user := range users {
if user.ID == id {
return user
}
}
panic("user not found") // 错误!这应该返回error
}
// ✅ 正确:返回error
func findUser(users []User, id int) (User, error) {
for _, user := range users {
if user.ID == id {
return user, nil
}
}
return User{}, errors.New("user not found")
}
❌ 不要忽略recover的返回值
// ❌ 错误:忽略recover的具体错误
defer func() {
recover() // 忽略了panic的信息
}()
// ✅ 正确:处理recover的返回值
defer func() {
if r := recover(); r != nil {
log.Printf("Recovered from panic: %v", r)
// 适当的错误处理
}
}()
7. 调试和日志记录
import (
"log"
"runtime/debug"
)
// ✅ 完整的panic恢复和日志记录
func robustHandler(fn func()) {
defer func() {
if r := recover(); r != nil {
// 记录panic信息
log.Printf("PANIC: %v", r)
// 记录调用栈
log.Printf("Stack trace:\n%s", debug.Stack())
// 可以添加更多上下文信息
log.Printf("Goroutine ID: %d", getGoroutineID())
}
}()
fn()
}
// 获取goroutine ID的辅助函数
func getGoroutineID() int64 {
// 实现细节省略
return 0
}
总结
panic和recover的核心原则:
1. panic用于真正异常的情况,不是正常的错误处理
2. recover只能在defer中使用,用于防止程序崩溃
3. 优先使用error返回值进行错误处理
4. 在服务器/长期运行的程序中使用recover防止单个错误导致整个程序崩溃
5. 总是记录panic信息和调用栈用于调试
panic和recover是Go语言错误处理体系的补充,而不是替代。正确使用它们可以程序更加健壮。
引用链接
[1]
Go 语言 panic 和 recover 的原理 | Go 语言设计与实现: https://draven.co/golang/docs/part2-foundation/ch05-keyword/golang-panic-recover/[2]
panic与recover机制 | 深入Go语言之旅: https://go.cyub.vip/feature/panic-recover/
评论区