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 恢复逻辑