错误处理
本章深入Go错误处理:error接口、错误创建与传递、错误包装、panic与recover。重点:Go没有异常机制,错误是返回值;理解fmt.Errorf包装和errors.Unwrap解包、panic仅用于真正无法恢复的错误。
前置知识:第5章 函数 学习目标:掌握Go错误处理模式、理解panic vs error的适用场景
9.1 Go的error接口
Section titled “9.1 Go的error接口”error接口定义
Section titled “error接口定义”type error interface { Error() string}Go的错误只是实现Error()方法的接口。非常简单,但非常强大。
import "errors"
err := errors.New("文件未找到")fmt.Println(err.Error()) // "文件未找到"
// 方式2:fmt.Errorferr := fmt.Errorf("读取文件失败: %s", filename)import "errors"
// 标准库常用错误var ErrPermission = errors.New("permission denied")var ErrNotFound = errors.New("not found")var ErrExist = errors.New("file already exists")
// 使用if os.Open("test.txt") == nil { // 不存在,处理错误}9.2 错误创建与传递
Section titled “9.2 错误创建与传递”本节目的:掌握错误创建模式、错误链传递原则
基本错误模式
Section titled “基本错误模式”func readFile(filename string) error { file, err := os.Open(filename) if err != nil { return err // 简单传播 } defer file.Close()
// 处理文件... return nil}创建有意义的错误
Section titled “创建有意义的错误”func getUser(id int) (*User, error) { user, ok := users[id] if !ok { return nil, fmt.Errorf("用户ID %d 不存在", id) } return &user, nil}错误变量 vs 错误值
Section titled “错误变量 vs 错误值”// 不好:每次创建新errorif err != nil { return errors.New("用户未找到") // 无法用==比较}
// 好:使用预定义错误变量var ErrUserNotFound = errors.New("用户未找到")
if err == ErrUserNotFound { // 可以精确比较}错误类型判断
Section titled “错误类型判断”// 使用errors.As提取错误类型type PathError struct { Path string Err error}
func (e *PathError) Error() string { return e.Path + ": " + e.Err.Error()}
var perr *PathErrorif errors.As(err, &perr) { fmt.Println("路径错误:", perr.Path) fmt.Println("原始错误:", perr.Err)}9.3 错误包装与unwrap
Section titled “9.3 错误包装与unwrap”本节目的:理解错误包装的目的、掌握
%w和errors.Unwrap的用法
为什么需要包装
Section titled “为什么需要包装”原始错误丢失上下文:
// 不好:只知道"读取文件失败",不知道哪个文件return errors.New("读取文件失败")
// 好:包含文件名和原始错误return fmt.Errorf("读取文件 %s 失败: %w", filename, err)fmt.Errorf的%w
Section titled “fmt.Errorf的%w”import "errors"
err1 := errors.New("原始错误")err2 := fmt.Errorf("包装1: %w", err1)err3 := fmt.Errorf("包装2: %w", err2)
// 解包fmt.Println(errors.Unwrap(err3)) // 包装1: 原始错误fmt.Println(errors.Unwrap(errors.Unwrap(err3))) // 原始错误
// 判断错误链if errors.Is(err3, err1) { fmt.Println("err3包含err1") // true}错误链的实际应用
Section titled “错误链的实际应用”func readConfig() error { file, err := os.Open("config.json") if err != nil { return fmt.Errorf("打开配置文件失败: %w", err) } defer file.Close()
var cfg Config if err := json.NewDecoder(file).Decode(&cfg); err != nil { return fmt.Errorf("解析配置文件失败: %w", err) } return nil}
// 调用err := readConfig()if err != nil { if errors.Is(err, os.ErrNotExist) { fmt.Println("配置文件不存在") } else { fmt.Println("其他错误:", err) }}自定义错误类型包装
Section titled “自定义错误类型包装”type ValidationError struct { Field string Msg string}
func (e *ValidationError) Error() string { return fmt.Sprintf("%s: %s", e.Field, e.Msg)}
func (e *ValidationError) Unwrap() error { return errors.New("validation failed")}
// 检查错误链var ve *ValidationErrorif errors.As(err, &ve) { fmt.Println("验证错误:", ve.Field, ve.Msg)}9.4 panic与recover
Section titled “9.4 panic与recover”本节目的:理解panic的用途、掌握recover捕获时机
panic是什么
Section titled “panic是什么”panic是Go的错误处理机制,用于处理”不应该发生”的错误:
// 触发panicpanic("发生了严重错误")
// panic后可接任何值panic(err)panic的典型场景
Section titled “panic的典型场景”// 1. 初始化失败func init() { if os.Getenv("CONFIG_PATH") == "" { panic("CONFIG_PATH must be set") }}
// 2. 程序员错误(不应该发生)if obj == nil { panic("obj is nil - this is a bug")}
// 3. 不可恢复的错误(如数组越界是panic)// Go会自动对运行时错误触发panicPython的raise对比
Section titled “Python的raise对比”# Pythonraise ValueError("invalid value")raise Exception("something went wrong")recover捕获panic
Section titled “recover捕获panic”func safeCall() { // defer中的recover可以捕获panic defer func() { if r := recover(); r != nil { fmt.Println("捕获panic:", r) } }()
fmt.Println("开始") panic("oops") fmt.Println("不会执行")}
func main() { safeCall() fmt.Println("继续执行")}defer执行顺序
Section titled “defer执行顺序”panic时,defer按LIFO顺序执行:
func main() { defer fmt.Println("1") defer fmt.Println("2") defer fmt.Println("3")
panic("panic!")}
// 输出:3 2 1(后进先出)panic vs error
Section titled “panic vs error”| 场景 | 使用 | 原因 |
|---|---|---|
| 函数可能失败 | error返回 | 预期内的失败 |
| 文件不存在 | error | 可以处理 |
| 程序员的bug | panic | 不应该发生 |
| 初始化失败 | panic | 无法继续 |
封装Web服务器panic
Section titled “封装Web服务器panic”func handle(w http.ResponseWriter, r *http.Request) { defer func() { if err := recover(); err != nil { log.Printf("panic: %v", err) http.Error(w, "Internal Server Error", 500) } }()
// handler逻辑...}9.5 最佳实践与错误链
Section titled “9.5 最佳实践与错误链”本节目的:掌握Go错误处理最佳实践、避免常见反模式
错误处理原则
Section titled “错误处理原则”- 错误作为返回值:Go没有异常,错误通过返回值传递
- 不要忽略错误:至少记录或传播
- 添加上下文:用fmt.Errorf包装
- 区分错误类型:使用errors.Is/As
// 反模式1:忽略错误file, _ := os.Open("test.txt") // 不要这样
// 反模式2:只返回字符串return errors.New("文件未找到") // 无法分类
// 反模式3:panic用于正常错误if err != nil { panic(err) // 不要这样}
// 反模式4:多层嵌套if err != nil { return err // 层层返回,无法定位}正确的错误处理模式
Section titled “正确的错误处理模式”// 模式1:立即处理data, err := os.ReadFile("test.txt")if err != nil { return fmt.Errorf("读取配置失败: %w", err)}
// 模式2:操作失败时包装func process() error { if err := step1(); err != nil { return fmt.Errorf("步骤1失败: %w", err) } if err := step2(); err != nil { return fmt.Errorf("步骤2失败: %w", err) } return nil}
// 模式3:使用错误变量var ErrNotFound = errors.New("资源不存在")
func find(id string) (Resource, error) { r, ok := cache.Get(id) if !ok { return nil, ErrNotFound } return r, nil}// 汇总多个错误type MultiError struct { errors []error}
func (m *MultiError) Add(err error) { if err != nil { m.errors = append(m.errors, err) }}
func (m *MultiError) Error() string { if len(m.errors) == 0 { return "" } return fmt.Sprintf("%d errors occurred", len(m.errors))}
// 使用var errs MultiErrorfor _, task := range tasks { if err := task.Run(); err != nil { errs.Add(err) }}if len(errs.errors) > 0 { return &errs}import "log"
// 记录并返回if err != nil { log.Printf("读取文件失败: %v", err) return err}
// 使用log.Fatal记录致命错误(会退出程序)if err != nil { log.Fatalf("无法启动: %v", err)}- 错误检查不遗漏:每个
err != nil都要处理,不要用_忽略错误 - 包装而非隐藏:用
fmt.Errorf("context: %w", err)包装错误,保留原始错误信息 - panic仅用于真正异常:程序逻辑错误(如数组越界)用panic,业务错误(如文件不存在)用error
- defer recover捕获panic:recover只能在defer中生效,用于将panic转为可处理的error
- Go 1.20+用errors.Join:合并多个错误用
errors.Join,不必手动实现MultiError
| Python | Go |
|---|---|
| try/except | if err != nil |
| raise Exception | panic() |
| except Exception as e | errors.As(err, &target) |
| raise from | fmt.Errorf(“%w”, err) |
| 无 | errors.Is(err1, err2) |
| 无 | errors.Unwrap(err) |
Go错误处理哲学:
- 错误是值,不是异常
- 错误应该被传播和包装
- panic用于真正不应该发生的情况
- 实现一个函数,返回(int, error),处理除数为零的情况
- 创建自定义错误类型,包含错误码和消息
- 用fmt.Errorf包装错误,构建三层错误链
- 用errors.Is检查错误链中的特定错误
- 实现recover捕获panic的safe函数