目 录CONTENT

文章目录

Go语言里的panic和recover用法

Administrator
2025-09-09 / 0 评论 / 0 点赞 / 0 阅读 / 0 字

 

字数 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. 1. 停止当前函数的正常执行

  2. 2. 执行当前函数的所有defer语句(按LIFO顺序)

  3. 3. 返回到调用函数,重复步骤2

  4. 4. 一直向上传播,直到main函数

  5. 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. 1. 程序逻辑错误(编程错误,不应该发生)

  2. 2. 初始化失败(程序无法继续运行)

  3. 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. 1. 预期可能发生的错误

  2. 2. 用户输入错误

  3. 3. 网络/IO错误

  4. 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. 1. panic用于真正异常的情况,不是正常的错误处理

  2. 2. recover只能在defer中使用,用于防止程序崩溃

  3. 3. 优先使用error返回值进行错误处理

  4. 4. 在服务器/长期运行的程序中使用recover防止单个错误导致整个程序崩溃

  5. 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/

 

0
Go
  1. 支付宝打赏

    qrcode alipay
  2. 微信打赏

    qrcode weixin

评论区