← 标准库最佳实践 | OOM 分析 →

Panic 处理 - 异常处理最佳实践

Panic 是 Go 中的严重错误,会导致程序终止。理解 panic 的机制、何时使用 panic、以及如何 recover 是编写健壮 Go 程序的关键。

📌 核心概念

🚨

Panic

程序严重错误

panic(err)
🛡️

Recover

恢复 panic

recover()
📞

Defer

延迟执行

defer fn()
📋

Stack Trace

调用栈信息

runtime/debug

Panic 机制

📖 Panic 的执行流程

package main

import "fmt"

func main() {
    a()
    fmt.Println("This won't print")
}

func a() {
    defer fmt.Println("defer in a")
    b()
}

func b() {
    defer fmt.Println("defer in b")
    c()
}

func c() {
    defer fmt.Println("defer in c")
    panic("something went wrong!")
    // panic 后的代码不会执行
}

// 输出顺序:
// 1. defer in c
// 2. defer in b
// 3. defer in a
// 4. panic 信息和 stack trace
panic 发生
停止当前函数
执行 defer
返回上层函数
执行 defer
继续向上
到达 main?
程序终止
输出 stack trace

何时使用 Panic

⚠️ Panic 使用原则

  • 真正异常: 程序无法继续执行的错误
  • 初始化失败: 关键资源初始化失败
  • 不可达代码: 理论上不会执行的分支
  • 开发错误: 调用者使用 API 错误
  • 避免滥用: 不要用 panic 处理预期错误

📝 Panic 的合理使用场景

package main

import (
    "fmt"
    "net/http"
)

// 场景 1: 初始化失败 (无法恢复)
var config map[string]string

func init() {
    if err := loadConfig(); err != nil {
        panic(fmt.Sprintf("Failed to load config: %v", err))
    }
}

func loadConfig() error {
    // 模拟加载配置
    return fmt.Errorf("config file not found")
}

// 场景 2: 不可达代码
func handleType(t string) {
    switch t {
    case "A":
        // 处理 A
    case "B":
        // 处理 B
    default:
        panic(fmt.Sprintf("unreachable: unknown type %s", t))
    }
}

// 场景 3: 开发错误 (API 误用)
func NewServer(addr string) *http.Server {
    if addr == "" {
        panic("addr must not be empty")
    }
    return &http.Server{Addr: addr}
}

// 场景 4: 必须有的值缺失
func getRequired(m map[string]string, key string) string {
    val, ok := m[key]
    if !ok {
        panic(fmt.Sprintf("required key %s not found", key))
    }
    return val
}

func main() {
    // 这些场景 panic 是合理的
}

Recover 恢复 Panic

📝 Recover 的正确用法

package main

import (
    "fmt"
    "runtime/debug"
)

// recover 必须在 defer 中调用
func safeFunction() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Printf("Recovered: %v\n", r)
            fmt.Println(string(debug.Stack()))
        }
    }()
    
    panic("test panic")
    // panic 后的代码不会执行
}

// Web 服务中的 panic 恢复
func middleware(next func()) {
    defer func() {
        if r := recover(); r != nil {
            fmt.Printf("Panic recovered: %v\n", r)
            // 记录日志、发送告警等
        }
    }()
    next()
}

func main() {
    safeFunction()
    fmt.Println("Program continues...")
}

⚠️ Recover 注意事项

  • 必须在 defer 中: 直接调用 recover() 返回 nil
  • 同一 goroutine: 只能恢复当前 goroutine 的 panic
  • 检查 nil: recover() 返回 nil 表示没有 panic
  • 不要滥用: 不是所有 panic 都应该恢复
  • 记录日志: 恢复后应记录错误和 stack trace

生产级 Panic 处理模式

1. Goroutine Panic 恢复

📝 保护 Goroutine

package main

import (
    "fmt"
    "runtime/debug"
    "time"
)

// 安全启动 Goroutine
func goSafe(fn func()) {
    go func() {
        defer func() {
            if r := recover(); r != nil {
                fmt.Printf("Goroutine panic: %v\n", r)
                fmt.Println(string(debug.Stack()))
                // 发送告警、记录日志
            }
        }()
        fn()
    }()
}

func main() {
    // 正常 Goroutine
    goSafe(func() {
        fmt.Println("Working...")
    })
    
    // 会 panic 的 Goroutine
    goSafe(func() {
        panic("something wrong")
    })
    
    time.Sleep(time.Second)
    fmt.Println("Main continues")
}

2. HTTP 中间件

📝 HTTP Panic 恢复中间件

package main

import (
    "fmt"
    "net/http"
    "runtime/debug"
)

func recoveryMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if err := recover(); err != nil {
                fmt.Printf("Panic: %v\n", err)
                fmt.Println(string(debug.Stack()))
                
                http.Error(w, "Internal Server Error", http.StatusInternalServerError)
            }
        }()
        next.ServeHTTP(w, r)
    })
}

func handler(w http.ResponseWriter, r *http.Request) {
    panic("unexpected error")
}

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/", handler)
    
    http.ListenAndServe(":8080", recoveryMiddleware(mux))
}

3. 批量任务 Panic 隔离

📝 隔离 Panic 不影响其他任务

package main

import (
    "fmt"
    "sync"
)

func processTask(id int) (error) {
    if id == 3 {
        panic("task 3 failed")
    }
    fmt.Printf("Task %d completed\n", id)
    return nil
}

func processAll(ids []int) {
    var wg sync.WaitGroup
    
    for _, id := range ids {
        wg.Add(1)
        go func(id int) {
            defer func() {
                if r := recover(); r != nil {
                    fmt.Printf("Task %d panic: %v\n", id, r)
                }
                wg.Done()
            }()
            
            processTask(id)
        }(id)
    }
    
    wg.Wait()
}

func main() {
    processAll([]int{1, 2, 3, 4, 5})
    // 任务 3 panic,但其他任务正常完成
}

错误 vs Panic

📊 Error 和 Panic 的选择

// ✅ 使用 error 的场景 (预期错误)
func ReadFile(path string) ([]byte, error) {
    data, err := os.ReadFile(path)
    if err != nil {
        return nil, err // 文件不存在是预期错误
    }
    return data, nil
}

// ✅ 使用 panic 的场景 (异常错误)
func MustParseConfig(path string) *Config {
    config, err := ParseConfig(path)
    if err != nil {
        panic(fmt.Sprintf("invalid config: %v", err))
    }
    return config
}

// 对比表
// +-------------+-------------+-------------+
// | 场景        | error       | panic       |
// +-------------+-------------+-------------+
// | 文件不存在   | ✅ 预期     | ❌ 过度     |
// | 网络超时     | ✅ 预期     | ❌ 过度     |
// | 参数验证失败 | ✅ 预期     | ❌ 过度     |
// | 配置加载失败 | ❌ 可恢复   | ✅ 关键资源 |
// | 空指针       | ❌ 应检查   | ✅ 开发错误 |
// | 不可达代码   | ❌ 无法处理 | ✅ 合理     |
// +-------------+-------------+-------------+

✅ Panic 处理最佳实践

  • 慎用 panic: 优先使用 error 返回
  • 边界恢复: 在程序边界 (main、goroutine、HTTP) 恢复
  • 记录日志: 恢复时记录 panic 信息和 stack trace
  • 发送告警: 生产环境 panic 应触发告警
  • 不要屏蔽: 不要静默吞掉 panic
  • 测试覆盖: 测试 panic 恢复逻辑

总结