Files
cs-note/hzh/GolangStar/Go语言基础/Go语言error.md
T

181 lines
4.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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`) |