--- tags: [go, golang, go基础语法, error] create time: 2026-06-07 15:00 --- # Go 语言 error ## 概述 `error` 是 Go 的内置接口(只有一个 `Error() string` 方法)。本文讲解 error 的本质、创建方式、自定义 error、错误包装与比较。 ## 正文 ### error 到底是什么? ```go type error interface { Error() string } ``` **error 就是一个接口**。任何实现了 `Error() string` 方法的类型都可以作为 error 返回。这意味着你可以完全自定义错误类型。 > [!question] ❓ 思考 > 既然 error 只是一个接口,那为什么不能直接用 `==` 比较两个 error? ### 创建 error 的两种方式 #### errors.New() — 简单错误 ```go err := errors.New("something went wrong") ``` 底层实现: ```go func New(text string) error { return &errorString{text} // 返回指针 } ``` #### fmt.Errorf() — 带格式化的错误 ```go err := fmt.Errorf("failed to open file %s: %v", path, innerErr) ``` 支持 `%w` 包装错误(见下文"错误包装"章节)。 > [!warning] ⚠️ error 不能用 == 直接比较 > `errors.New("hello") == errors.New("hello")` 结果是 `false`,因为它们是不同的指针实例。需要比较字符串内容:`err.Error() == "hello"`。 ### 标准错误处理模式 Go 约定:**error 作为最后一个返回值**: ```go func ReadFile(name string) (string, error) { data, err := os.ReadFile(name) if err != nil { return "", err // 向上层传递错误 } return string(data), nil } // 调用方 content, err := ReadFile("config.json") if err != nil { log.Fatal(err) } ``` > [!tip] 💡 防御性编程 > ```go > if err != nil { > return err // 尽早返回,避免嵌套 > } > // 下面是正常逻辑 > ``` > 这就是著名的 "error laddering"——每个错误处理块都缩进一级,最终正常逻辑在最外层。 ### 自定义 error 实现 `Error() string` 方法即可: ```go type NotFoundError struct { Resource string } func (e *NotFoundError) Error() string { return fmt.Sprintf("resource not found: %s", e.Resource) } // 使用 err := &NotFoundError{Resource: "user_42"} fmt.Println(err) // resource not found: user_42 ``` 如果需要携带额外信息(如错误码): ```go type AppError struct { Code int Msg string } func (e *AppError) Error() string { return fmt.Sprintf("[%d] %s", e.Code, e.Msg) } // 提取错误码 func ErrorCode(err error) int { if appErr, ok := err.(*AppError); ok { return appErr.Code } return -1 } ``` > [!note] 📝 用类型断言提取信息 > `err.(*AppError)` 将 error 接口还原为具体类型,可以访问额外的字段。 ### 错误包装(Go 1.13+) `fmt.Errorf` 支持 `%w` 来**包装**错误,保留原始错误链: ```go func LoadConfig(path string) error { data, err := os.ReadFile(path) if err != nil { return fmt.Errorf("load config: %w", err) } return nil } // 检查是否包含特定错误 var fsErr *fs.PathError if errors.As(err, &fsErr) { fmt.Printf("文件系统错误: %v\n", fsErr) } // 检查错误链中是否有某个错误 if errors.Is(err, io.EOF) { fmt.Println("到达文件末尾") } ``` > [!info] ℹ️ 三个核心函数 > | 函数 | 用途 | > |------|------| > | `errors.Is(err, target)` | 检查 err 是否在错误链中等于 target | > | `errors.As(err, target)` | 将 err 链中的第一个匹配类型赋值给 target | > | `fmt.Errorf("%w", err)` | 包装错误,保留原始错误 | ```mermaid graph TD A["load config 错误"] --> B["os.Open 错误"] B --> C["permission denied"] style A fill:#f9f,stroke:#333 style B fill:#bbf,stroke:#333 style C fill:#ff9,stroke:#333 ``` > [!warning] ⚠️ %w vs %v > - `%w` — **包装**错误,可以用 `errors.Is/As` 追溯 > - `%v` — 仅格式化输出,不保留错误链关系 ### 最佳实践总结 | 场景 | 推荐做法 | |------|---------| | 简单错误 | `errors.New()` | | 带上下文的错误 | `fmt.Errorf("context: %w", err)` | | 需要提取错误码 | 自定义 struct + `Error() string` | | 检查错误类型 | `errors.As(err, &target)` | | 判断是否某类错误 | `errors.Is(err, target)` | | 业务逻辑错误 | 定义专门的错误类型(如 `ErrNotFound`) |