Skip to content

panic 与 recover

1. 概述

panicrecover 是 Go 语言中用于错误处理的特殊机制。panic 用于引发程序的异常状态,而 recover 用于捕获并处理这些异常,使程序能够从异常状态中恢复并继续执行。

在 Go 语言中,panicrecover 通常与 defer 语句配合使用,形成一种 "延迟处理异常" 的模式。这种模式使得 Go 语言能够以一种更加灵活和可控的方式处理程序中的异常情况,而不需要像其他语言那样依赖于传统的异常处理机制(如 try-catch)。

本章节将详细介绍 Go 语言中 panicrecover 的定义、使用方法和最佳实践,帮助读者掌握这一重要特性。

2. 基本概念

2.1 语法

Go 语言中 panicrecover 的基本语法结构如下:

go
// panic 语法
panic(错误信息)

// recover 语法(通常与 defer 配合使用)
defer func() {
    if r := recover(); r != nil {
        // 处理 panic
        fmt.Println("恢复 from panic:", r)
    }
}()

// 完整示例
func example() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("恢复 from panic:", r)
        }
    }()
    
    // 引发 panic
    panic("发生错误")
}
  • panic 函数接收一个任意类型的参数,通常是字符串或错误对象,用于描述错误信息
  • recover 函数没有参数,它返回 panic 函数传递的参数
  • recover 函数只能在 defer 语句中使用,在其他地方使用会返回 nil
  • recover 捕获到 panic 时,它会返回 panic 的参数,否则返回 nil

2.2 语义

panicrecover 的语义如下:

  • panic:用于引发程序的异常状态,当 panic 被调用时,程序会立即停止执行当前函数的剩余代码,开始执行 defer 栈中的函数调用,然后将异常向上传播给调用者
  • recover:用于捕获并处理 panic,当 recover 被调用时,如果当前 goroutine 正在发生 panic,它会捕获这个 panic 并返回 panic 的参数,否则返回 nil
  • 传播机制:当 panic 发生时,它会沿着调用栈向上传播,直到被 recover 捕获或到达程序的顶层,导致程序崩溃
  • 与 defer 的配合:recover 通常与 defer 语句配合使用,因为 defer 语句会在函数返回前执行,包括在 panic 发生时

2.3 规范

  • panic 应该用于处理不可恢复的错误,而不是用于常规的错误处理
  • 常规的错误应该使用 error 类型返回,而不是使用 panic
  • recover 应该用于捕获并处理 panic,使程序能够从异常状态中恢复
  • recover 应该在 defer 语句中使用,以确保它能够捕获到所有的 panic
  • 当使用 recover 捕获 panic 时,应该记录错误信息,以便于调试和监控

3. 原理深度解析

3.1 panic 的实现机制

在 Go 语言中,panic 的实现依赖于运行时的 panic 机制。当 panic 被调用时,运行时会:

  1. 创建一个 panic 对象,包含错误信息和堆栈信息
  2. 立即停止执行当前函数的剩余代码
  3. 开始执行 defer 栈中的函数调用(按后进先出顺序)
  4. 将 panic 向上传播给调用者
  5. 如果 panic 到达程序的顶层(没有被 recover 捕获),程序会崩溃并打印错误信息和堆栈跟踪

3.2 recover 的实现机制

recover 函数的实现也依赖于运行时的 panic 机制。当 recover 被调用时,运行时会:

  1. 检查当前 goroutine 是否正在发生 panic
  2. 如果是,获取 panic 的参数并停止 panic 的传播
  3. 如果不是,返回 nil

3.3 panic 与 defer 的关系

panicdefer 密切相关。当 panic 发生时,Go 运行时会执行 defer 栈中的所有函数调用,然后再将 panic 向上传播。这使得 defer 语句成为处理 panic 的理想场所,因为它确保了无论函数是正常返回还是因为 panic 而返回,defer 语句都会执行。

4. 常见错误与踩坑点

4.1 错误表现

  • 过度使用 panic 处理常规错误
  • 在非 defer 语句中使用 recover 函数
  • recover 后没有正确处理错误,导致程序状态不一致
  • panic 信息不够详细,导致调试困难
  • 没有在适当的地方使用 recover,导致程序崩溃

4.2 产生原因

  • 对 panic 和 recover 的使用场景理解不正确
  • 不了解 recover 函数的使用限制(只能在 defer 语句中使用)
  • 没有考虑到 recover 后的程序状态一致性问题
  • 疏忽了错误信息的重要性
  • 对程序的异常处理策略设计不完善

4.3 解决方案

  • 只在处理不可恢复的错误时使用 panic,常规错误应该使用 error 类型返回
  • 确保 recover 函数只在 defer 语句中使用
  • 在 recover 后,确保程序状态的一致性,避免继续使用可能已经损坏的状态
  • 提供详细的 panic 信息,包括错误的上下文和相关数据
  • 设计完善的异常处理策略,在适当的地方使用 recover 来捕获和处理 panic

5. 常见应用场景

5.1 场景一:处理不可恢复的错误

场景描述:当程序遇到不可恢复的错误时,使用 panic 来终止程序的执行。

使用方法:直接调用 panic 函数,传递错误信息。

示例代码

go
package main

import "fmt"

func main() {
    fmt.Println("开始执行")
    
    // 模拟不可恢复的错误
    if true {
        panic("遇到不可恢复的错误,程序终止")
    }
    
    fmt.Println("这里不会执行")
}

5.2 场景二:在 defer 中使用 recover 捕获 panic

场景描述:使用 defer 和 recover 捕获并处理 panic,使程序能够从异常状态中恢复。

使用方法:在 defer 语句中使用 recover 函数捕获 panic。

示例代码

go
package main

import "fmt"

func safeFunction() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("恢复 from panic:", r)
        }
    }()
    
    fmt.Println("执行中...")
    panic("发生错误")
    fmt.Println("这里不会执行")
}

func main() {
    fmt.Println("调用 safeFunction")
    safeFunction()
    fmt.Println("程序继续执行")
}

5.3 场景三:实现自定义的错误处理机制

场景描述:使用 panic 和 recover 实现自定义的错误处理机制,例如处理 HTTP 请求中的错误。

使用方法:在适当的地方使用 panic 引发错误,然后在顶层使用 recover 捕获并处理这些错误。

示例代码

go
package main

import "fmt"

// AppError 应用错误
type AppError struct {
    Code    int
    Message string
}

func (e *AppError) Error() string {
    return fmt.Sprintf("错误 %d: %s", e.Code, e.Message)
}

// 模拟 HTTP 处理函数
func handleRequest() {
    defer func() {
        if r := recover(); r != nil {
            switch err := r.(type) {
            case *AppError:
                fmt.Printf("处理应用错误: 代码=%d, 消息=%s\n", err.Code, err.Message)
            default:
                fmt.Printf("处理未知错误: %v\n", r)
            }
        }
    }()
    
    // 模拟业务逻辑错误
    if true {
        panic(&AppError{Code: 404, Message: "资源不存在"})
    }
}

func main() {
    fmt.Println("处理请求")
    handleRequest()
    fmt.Println("请求处理完成")
}

5.4 场景四:在测试中使用 panic

场景描述:在测试中使用 panic 来表示测试失败。

使用方法:在测试函数中,当测试条件不满足时,使用 panic 来表示测试失败。

示例代码

go
package main

import "fmt"

// 模拟测试函数
func TestAdd() {
    result := Add(1, 2)
    if result != 3 {
        panic(fmt.Sprintf("测试失败: 期望 3,得到 %d", result))
    }
    fmt.Println("测试通过")
}

// 被测试的函数
func Add(a, b int) int {
    return a + b
}

func main() {
    fmt.Println("运行测试")
    
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("测试失败:", r)
        }
    }()
    
    TestAdd()
    fmt.Println("所有测试通过")
}

5.5 场景五:处理并发中的 panic

场景描述:在并发编程中,处理 goroutine 中的 panic,避免单个 goroutine 的 panic 导致整个程序崩溃。

使用方法:在每个 goroutine 的入口处使用 defer 和 recover 捕获并处理 panic。

示例代码

go
package main

import (
    "fmt"
    "sync"
)

func worker(id int, wg *sync.WaitGroup) {
    defer wg.Done()
    
    // 捕获并处理 panic
    defer func() {
        if r := recover(); r != nil {
            fmt.Printf("Worker %d 发生错误: %v\n", id, r)
        }
    }()
    
    fmt.Printf("Worker %d 开始执行\n", id)
    
    // 模拟 panic
    if id == 2 {
        panic(fmt.Sprintf("Worker %d 遇到错误", id))
    }
    
    fmt.Printf("Worker %d 执行完成\n", id)
}

func main() {
    var wg sync.WaitGroup
    
    // 启动 5 个 worker
    for i := 1; i <= 5; i++ {
        wg.Add(1)
        go worker(i, &wg)
    }
    
    fmt.Println("等待所有 worker 完成")
    wg.Wait()
    fmt.Println("所有 worker 完成")
}

6. 企业级进阶应用场景

6.1 场景一:实现统一的错误处理中间件

场景描述:在企业级应用中,实现统一的错误处理中间件,用于捕获和处理所有的 panic。

使用方法:在中间件中使用 defer 和 recover 捕获 panic,然后将其转换为适当的错误响应。

示例代码

go
package main

import (
    "fmt"
    "net/http"
)

// ErrorResponse 错误响应结构
type ErrorResponse struct {
    Code    int    `json:"code"`
    Message string `json:"message"`
}

// ErrorHandler 错误处理中间件
func ErrorHandler(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if r := recover(); r != nil {
                // 记录错误
                fmt.Printf("捕获到 panic: %v\n", r)
                
                // 返回 500 错误
                w.Header().Set("Content-Type", "application/json")
                w.WriteHeader(http.StatusInternalServerError)
                fmt.Fprintf(w, `{"code": 500, "message": "内部服务器错误"}`)
            }
        }()
        
        next.ServeHTTP(w, r)
    })
}

// 模拟业务处理函数
func handler(w http.ResponseWriter, r *http.Request) {
    // 模拟 panic
    panic("业务逻辑错误")
}

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/api", handler)
    
    // 应用错误处理中间件
    http.ListenAndServe(":8080", ErrorHandler(mux))
}

6.2 场景二:实现事务管理

场景描述:在企业级应用中,实现数据库事务管理,确保事务在遇到错误时能够正确回滚。

使用方法:使用 defer 和 recover 捕获事务执行过程中的 panic,确保事务能够正确回滚。

示例代码

go
package main

import (
    "database/sql"
    "fmt"
)

// Transaction 执行事务
func Transaction(db *sql.DB, fn func(*sql.Tx) error) error {
    tx, err := db.Begin()
    if err != nil {
        return err
    }
    
    // 延迟处理事务
    defer func() {
        if r := recover(); r != nil {
            // 发生 panic,回滚事务
            tx.Rollback()
            err = fmt.Errorf("事务执行过程中发生 panic: %v", r)
        } else if err != nil {
            // 发生错误,回滚事务
            tx.Rollback()
        } else {
            // 执行成功,提交事务
            err = tx.Commit()
        }
    }()
    
    // 执行事务操作
    err = fn(tx)
    return err
}

func main() {
    // 这里只是示例,实际使用时需要正确初始化数据库连接
    // db, err := sql.Open("mysql", "user:password@tcp(localhost:3306)/dbname")
    // if err != nil {
    //     fmt.Println("连接数据库失败:", err)
    //     return
    // }
    // defer db.Close()
    
    // // 执行事务
    // err = Transaction(db, func(tx *sql.Tx) error {
    //     // 执行 SQL 语句
    //     _, err := tx.Exec("INSERT INTO users (name, age) VALUES (?, ?)", "Alice", 25)
    //     if err != nil {
    //         return err
    //     }
    //     
    //     // 模拟 panic
    //     panic("事务执行过程中发生错误")
    //     
    //     _, err = tx.Exec("INSERT INTO orders (user_id, amount) VALUES (?, ?)", 1, 100)
    //     return err
    // })
    // 
    // if err != nil {
    //     fmt.Println("事务执行失败:", err)
    // } else {
    //     fmt.Println("事务执行成功")
    // }
}

6.3 场景三:实现安全的工作池

场景描述:在企业级应用中,实现安全的工作池,确保单个工作的 panic 不会影响整个工作池。

使用方法:在每个工作协程中使用 defer 和 recover 捕获 panic,确保工作池的稳定性。

示例代码

go
package main

import (
    "fmt"
    "sync"
)

// WorkerPool 工作池
type WorkerPool struct {
    jobs    chan func()
    wg      sync.WaitGroup
}

// NewWorkerPool 创建工作池
func NewWorkerPool(size int) *WorkerPool {
    pool := &WorkerPool{
        jobs: make(chan func(), 100),
    }
    
    // 启动工作协程
    for i := 1; i <= size; i++ {
        pool.wg.Add(1)
        go pool.worker(i)
    }
    
    return pool
}

// worker 工作协程
func (p *WorkerPool) worker(id int) {
    defer p.wg.Done()
    
    for job := range p.jobs {
        // 捕获并处理 panic
        defer func() {
            if r := recover(); r != nil {
                fmt.Printf("Worker %d 发生错误: %v\n", id, r)
            }
        }()
        
        // 执行工作
        job()
    }
}

// Submit 提交工作
func (p *WorkerPool) Submit(job func()) {
    p.jobs <- job
}

// Close 关闭工作池
func (p *WorkerPool) Close() {
    close(p.jobs)
    p.wg.Wait()
}

func main() {
    // 创建工作池
    pool := NewWorkerPool(3)
    
    // 提交工作
    for i := 1; i <= 5; i++ {
        jobID := i
        pool.Submit(func() {
            fmt.Printf("执行工作 %d\n", jobID)
            
            // 模拟 panic
            if jobID == 3 {
                panic(fmt.Sprintf("工作 %d 发生错误", jobID))
            }
            
            fmt.Printf("工作 %d 执行完成\n", jobID)
        })
    }
    
    // 关闭工作池
    pool.Close()
    fmt.Println("所有工作执行完成")
}

7. 行业最佳实践

7.1 实践一:只在不可恢复的错误时使用 panic

实践内容:只在处理不可恢复的错误时使用 panic,常规错误应该使用 error 类型返回。

推荐理由panic 会中断程序的正常执行流程,应该只用于处理真正的异常情况,而不是用于常规的错误处理。

示例代码

go
// 正确:使用 error 返回常规错误
func Divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, errors.New("除数不能为零")
    }
    return a / b, nil
}

// 正确:使用 panic 处理不可恢复的错误
func LoadConfig() {
    config, err := readConfigFile()
    if err != nil {
        panic("无法加载配置文件,程序无法继续执行")
    }
    // 使用配置
}

7.2 实践二:使用 defer 和 recover 捕获 panic

实践内容:在适当的地方使用 defer 和 recover 捕获 panic,使程序能够从异常状态中恢复。

推荐理由:这样可以提高程序的健壮性,避免单个组件的故障导致整个程序崩溃。

示例代码

go
func main() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("程序从 panic 中恢复:", r)
            // 可以在这里添加日志记录、错误报告等
        }
    }()
    
    // 程序逻辑
    // ...
}

7.3 实践三:提供详细的 panic 信息

实践内容:在使用 panic 时,提供详细的错误信息,包括错误的上下文和相关数据。

推荐理由:详细的错误信息有助于调试和问题定位,提高程序的可维护性。

示例代码

go
// 不推荐:错误信息不够详细
panic("错误")

// 推荐:提供详细的错误信息
panic(fmt.Sprintf("处理用户 %s 的请求时发生错误: %v", userID, err))

7.4 实践四:在 recover 后确保程序状态的一致性

实践内容:在 recover 捕获到 panic 后,确保程序状态的一致性,避免继续使用可能已经损坏的状态。

推荐理由:当 panic 发生时,程序的状态可能已经不一致,需要确保在 recover 后程序能够安全地继续执行。

示例代码

go
defer func() {
    if r := recover(); r != nil {
        fmt.Println("恢复 from panic:", r)
        // 重置状态,确保一致性
        resetState()
    }
}()

7.5 实践五:在适当的层级使用 recover

实践内容:在适当的层级使用 recover 捕获 panic,通常是在组件的边界或程序的顶层。

推荐理由:这样可以确保局部的错误不会影响整个系统的稳定性,同时也便于集中处理错误。

示例代码

go
// 在 HTTP 处理函数中使用 recover
func handler(w http.ResponseWriter, r *http.Request) {
    defer func() {
        if r := recover(); r != nil {
            // 记录错误并返回 500 响应
            http.Error(w, "内部服务器错误", http.StatusInternalServerError)
        }
    }()
    
    // 处理请求
    // ...
}

8. 常见问题答疑(FAQ)

8.1 问题一:panic 和 error 有什么区别?

回答内容panic 用于引发程序的异常状态,会中断程序的正常执行流程;而 error 是一种常规的错误处理机制,用于表示程序执行过程中的错误状态,不会中断程序的正常执行流程。

使用场景

  • error:用于常规的错误处理,如文件打开失败、网络连接失败等
  • panic:用于处理不可恢复的错误,如配置文件加载失败、数据库连接失败等

8.2 问题二:recover 函数只能在 defer 语句中使用吗?

回答内容:是的,recover 函数只能在 defer 语句中使用。在其他地方使用 recover 函数会返回 nil,无法捕获 panic

示例代码

go
// 正确:在 defer 语句中使用 recover
func example() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("恢复 from panic:", r)
        }
    }()
    panic("发生错误")
}

// 错误:在非 defer 语句中使用 recover
func wrongExample() {
    if r := recover(); r != nil {
        fmt.Println("恢复 from panic:", r)
    }
    panic("发生错误")
}

8.3 问题三:panic 会向上传播吗?

回答内容:是的,当 panic 发生时,它会沿着调用栈向上传播,直到被 recover 捕获或到达程序的顶层,导致程序崩溃。

示例代码

go
func level3() {
    panic("发生错误")
}

func level2() {
    level3()
}

func level1() {
    level2()
}

func main() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("在 main 中恢复 from panic:", r)
        }
    }()
    level1()
}

8.4 问题四:多个 defer 语句的执行顺序是什么?

回答内容:多个 defer 语句按后进先出(LIFO)的顺序执行,即最后声明的 defer 语句最先执行。当 panic 发生时,也会按照这个顺序执行 defer 语句。

示例代码

go
func example() {
    defer fmt.Println("defer 1")
    defer fmt.Println("defer 2")
    defer fmt.Println("defer 3")
    panic("发生错误")
}

// 输出:
// defer 3
// defer 2
// defer 1
// panic: 发生错误

8.5 问题五:recover 后,程序的执行流程是什么?

回答内容:当 recover 捕获到 panic 后,程序会从 defer 语句之后继续执行,而不是从 panic 语句之后继续执行。

示例代码

go
func example() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("恢复 from panic:", r)
        }
    }()
    
    fmt.Println("执行步骤 1")
    panic("发生错误")
    fmt.Println("执行步骤 2") // 这里不会执行
}

func main() {
    example()
    fmt.Println("执行步骤 3") // 这里会执行
}

// 输出:
// 执行步骤 1
// 恢复 from panic: 发生错误
// 执行步骤 3

8.6 问题六:在并发编程中,一个 goroutine 的 panic 会影响其他 goroutine 吗?

回答内容:不会,一个 goroutine 的 panic 只会影响该 goroutine 本身,不会影响其他 goroutine 的执行。但是,如果主 goroutine 发生 panic 且没有被 recover 捕获,整个程序会崩溃。

示例代码

go
func main() {
    // 启动一个 goroutine
    go func() {
        defer func() {
            if r := recover(); r != nil {
                fmt.Println("goroutine 中的 panic 被捕获:", r)
            }
        }()
        panic("goroutine 中的错误")
    }()
    
    // 主 goroutine 继续执行
    fmt.Println("主 goroutine 继续执行")
    time.Sleep(1 * time.Second)
    fmt.Println("主 goroutine 执行完成")
}

9. 实战练习

9.1 基础练习

题目:实现一个函数,使用 panic 和 recover 处理除零错误。

解题思路:在函数中使用 panic 模拟除零错误,然后使用 recover 捕获并处理这个错误。

常见误区:没有正确使用 defer 语句,或者在非 defer 语句中使用 recover 函数。

分步提示

  1. 定义一个函数,接收两个整数参数
  2. 在函数中,当除数为零时,使用 panic 引发错误
  3. 使用 defer 语句和 recover 函数捕获并处理这个错误
  4. 返回计算结果或错误信息

参考代码

go
import "fmt"

func SafeDivide(a, b int) (int, error) {
    var result int
    var err error
    
    defer func() {
        if r := recover(); r != nil {
            err = fmt.Errorf("发生错误: %v", r)
        }
    }()
    
    if b == 0 {
        panic("除数不能为零")
    }
    
    result = a / b
    return result, err
}

func main() {
    result, err := SafeDivide(10, 2)
    if err != nil {
        fmt.Println("错误:", err)
    } else {
        fmt.Println("结果:", result)
    }
    
    result, err = SafeDivide(10, 0)
    if err != nil {
        fmt.Println("错误:", err)
    } else {
        fmt.Println("结果:", result)
    }
}

9.2 进阶练习

题目:实现一个函数,使用 panic、recover 和 defer 模拟一个简单的事务处理系统。

解题思路:使用 panic 模拟事务执行过程中的错误,然后使用 defer 和 recover 确保事务能够正确回滚。

常见误区:没有正确处理事务的提交和回滚逻辑,或者在 recover 后没有正确处理错误。

分步提示

  1. 定义一个事务结构体,包含提交和回滚方法
  2. 实现一个函数,用于执行事务
  3. 在函数中,使用 defer 和 recover 捕获事务执行过程中的错误
  4. 根据执行结果,决定是提交还是回滚事务

参考代码

go
import "fmt"

// Transaction 模拟事务
type Transaction struct {
    committed bool
}

// Commit 提交事务
func (t *Transaction) Commit() {
    t.committed = true
    fmt.Println("事务提交")
}

// Rollback 回滚事务
func (t *Transaction) Rollback() {
    fmt.Println("事务回滚")
}

// NewTransaction 创建事务
func NewTransaction() *Transaction {
    return &Transaction{}
}

// ExecuteTransaction 执行事务
func ExecuteTransaction(f func(*Transaction) error) error {
    tx := NewTransaction()
    var err error
    
    defer func() {
        if r := recover(); r != nil {
            tx.Rollback()
            err = fmt.Errorf("事务执行过程中发生 panic: %v", r)
        } else if err != nil {
            tx.Rollback()
        } else {
            tx.Commit()
        }
    }()
    
    err = f(tx)
    return err
}

func main() {
    // 执行成功的事务
    fmt.Println("执行成功的事务")
    err := ExecuteTransaction(func(tx *Transaction) error {
        fmt.Println("执行事务操作 1")
        fmt.Println("执行事务操作 2")
        return nil
    })
    if err != nil {
        fmt.Println("错误:", err)
    }
    
    // 执行失败的事务(返回错误)
    fmt.Println("\n执行失败的事务(返回错误)")
    err = ExecuteTransaction(func(tx *Transaction) error {
        fmt.Println("执行事务操作 1")
        return fmt.Errorf("事务操作失败")
    })
    if err != nil {
        fmt.Println("错误:", err)
    }
    
    // 执行失败的事务(发生 panic)
    fmt.Println("\n执行失败的事务(发生 panic)")
    err = ExecuteTransaction(func(tx *Transaction) error {
        fmt.Println("执行事务操作 1")
        panic("事务操作发生 panic")
    })
    if err != nil {
        fmt.Println("错误:", err)
    }
}

9.3 挑战练习

题目:实现一个简单的 HTTP 服务器,使用 panic、recover 和 defer 处理请求过程中的错误。

解题思路:在 HTTP 处理函数中使用 panic 模拟错误,然后使用 defer 和 recover 捕获并处理这些错误,返回适当的错误响应。

常见误区:没有正确设置 HTTP 响应头,或者在 recover 后没有正确处理错误。

分步提示

  1. 导入必要的包(net/http、fmt 等)
  2. 实现一个错误处理中间件,使用 defer 和 recover 捕获 panic
  3. 实现一个业务处理函数,使用 panic 模拟错误
  4. 启动 HTTP 服务器,应用错误处理中间件

参考代码

go
import (
    "fmt"
    "net/http"
)

// ErrorHandler 错误处理中间件
func ErrorHandler(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if r := recover(); r != nil {
                // 记录错误
                fmt.Printf("捕获到 panic: %v\n", r)
                
                // 设置响应头
                w.Header().Set("Content-Type", "application/json")
                w.WriteHeader(http.StatusInternalServerError)
                
                // 返回错误响应
                fmt.Fprintf(w, `{"code": 500, "message": "内部服务器错误"}`)
            }
        }()
        
        next.ServeHTTP(w, r)
    })
}

// Handler 业务处理函数
func Handler(w http.ResponseWriter, r *http.Request) {
    // 模拟正常请求
    if r.URL.Query().Get("ok") == "true" {
        w.Header().Set("Content-Type", "application/json")
        fmt.Fprintf(w, `{"code": 200, "message": "请求成功"}`)
        return
    }
    
    // 模拟错误
    panic("业务逻辑错误")
}

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/api", Handler)
    
    // 应用错误处理中间件
    server := &http.Server{
        Addr:    ":8080",
        Handler: ErrorHandler(mux),
    }
    
    fmt.Println("服务器启动在 :8080")
    fmt.Println("测试成功请求: http://localhost:8080/api?ok=true")
    fmt.Println("测试失败请求: http://localhost:8080/api")
    
    server.ListenAndServe()
}

10. 知识点总结

10.1 核心要点

  • panic 用于引发程序的异常状态,当 panic 被调用时,程序会立即停止执行当前函数的剩余代码
  • recover 用于捕获并处理 panic,使程序能够从异常状态中恢复
  • recover 只能在 defer 语句中使用,在其他地方使用会返回 nil
  • panic 发生时,程序会执行 defer 栈中的函数调用,然后将异常向上传播
  • panicrecover 通常与 defer 语句配合使用,形成一种 "延迟处理异常" 的模式
  • 常规的错误应该使用 error 类型返回,而不是使用 panic
  • panic 应该用于处理不可恢复的错误,例如配置文件加载失败、数据库连接失败等

10.2 易错点回顾

  • 过度使用 panic 处理常规错误,而不是使用 error 类型返回
  • 在非 defer 语句中使用 recover 函数
  • recover 后没有正确处理错误,导致程序状态不一致
  • panic 信息不够详细,导致调试困难
  • 没有在适当的地方使用 recover,导致程序崩溃
  • 在并发编程中,没有为每个 goroutine 添加 recover 保护

11. 拓展参考资料

11.1 官方文档链接

11.2 进阶学习路径建议

  • 学习 panicrecoverdefer 的配合使用
  • 学习如何设计健壮的错误处理策略
  • 学习如何在企业级应用中使用 panicrecover
  • 学习如何在并发编程中处理 panic
  • 学习如何使用 panicrecover 实现自定义的异常处理机制