# Go 语言错误处理

Go 语言采用了与其他主流语言截然不同的错误处理方式：**显式处理，不依赖异常**。错误是普通的值，通过返回值传递，由调用者显式检查和处理。本篇将系统介绍 Go 的错误处理机制，从基础的 error 接口到高级的错误包装与判断。

## Go 的错误处理哲学

Go 认为错误是程序执行过程中正常可能出现的情况，不应该用异常来控制程序流程。典型的模式是：

```go
result, err := doSomething()
if err != nil {
    return err
}
// 使用 result
```

这种模式的优势：错误处理逻辑紧跟调用点、不会跳过中间代码、强制开发者思考每个可能出错的地方。

![错误处理流程](https://img.zhaojq.top/20260729161724338.png "错误处理流程")

## error 接口

Go 内置的 `error` 接口只有一个方法：`Error() string`。任何实现了该方法的类型都是合法的错误类型，函数通常将 `error` 作为最后一个返回值。

## 创建错误

### errors.New

最简单的方式是使用 `errors.New` 创建一个包含固定消息的错误：

```go
package main

import (
    "errors"
    "fmt"
)

func validateAge(age int) error {
    if age < 0 {
        return errors.New("年龄不能为负数")
    }
    if age > 150 {
        return errors.New("年龄不合理")
    }
    return nil
}

func main() {
    err := validateAge(-1)
    if err != nil {
        fmt.Println("错误:", err) // 错误: 年龄不能为负数
    }
}
```

### fmt.Errorf

当需要在错误消息中包含动态信息时，使用 `fmt.Errorf`：

```go
package main

import "fmt"

func divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, fmt.Errorf("除数不能为零 (a=%v, b=%v)", a, b)
    }
    return a / b, nil
}

func main() {
    _, err := divide(10, 0)
    if err != nil {
        fmt.Println("错误:", err) // 错误: 除数不能为零 (a=10, b=0)
    }
}
```

## 自定义错误类型

当需要携带更多信息或区分不同错误类型时，可以定义自己的错误类型：

```go
package main

import (
    "fmt"
    "time"
)

type TimeoutError struct {
    Duration time.Duration
    Message  string
}

func (e *TimeoutError) Error() string {
    return fmt.Sprintf("超时: %v 后 %s", e.Duration, e.Message)
}

func fetchData(timeout time.Duration) error {
    return &TimeoutError{Duration: timeout, Message: "等待服务器响应"}
}

func main() {
    err := fetchData(5 * time.Second)
    if err != nil {
        fmt.Println(err) // 超时: 5s 后 等待服务器响应
    }
}
```

## 错误判断：errors.Is

`errors.Is` 用于判断错误链中是否包含某个特定的目标错误，类似于 `==` 但能处理错误包装：

```go
package main

import (
    "errors"
    "fmt"
)

var ErrNotFound = errors.New("未找到")

func findUser(id int) error {
    if id <= 0 {
        return fmt.Errorf("查询用户 %d: %w", id, ErrNotFound)
    }
    return nil
}

func main() {
    err := findUser(-1)
    if errors.Is(err, ErrNotFound) {
        fmt.Println("用户不存在") // 输出: 用户不存在
    }
}
```

> **对比**：旧写法 `err == ErrNotFound` 无法穿透包装的错误，`errors.Is` 解决了这个问题。

## 错误包装：fmt.Errorf + %w

Go 1.13 引入了 `%w` 动词，可以在错误消息中包装另一个错误，形成错误链：

```go
package main

import (
    "errors"
    "fmt"
)

var ErrDatabase = errors.New("数据库错误")

func queryUser(id int) error {
    if id == 0 {
        return fmt.Errorf("查询用户失败: %w", ErrDatabase)
    }
    return nil
}

func getUser(id int) error {
    err := queryUser(id)
    if err != nil {
        return fmt.Errorf("获取用户信息失败: %w", err)
    }
    return nil
}

func main() {
    err := getUser(0)
    fmt.Println("错误:", err)
    // 输出: 错误: 获取用户信息失败: 查询用户失败: 数据库错误

    // errors.Is 可以穿透错误链
    fmt.Println("是数据库错误?", errors.Is(err, ErrDatabase)) // true
}
```

> **注意**：`%w` 只能包装 `error` 类型的值。如果只需格式化不包装，用 `%v` 或 `%s`。

## 错误解包：errors.As

`errors.As` 用于从错误链中提取特定类型的错误，类似于类型断言但能穿透包装：

```go
package main

import (
    "errors"
    "fmt"
)

type ValidationError struct {
    Field   string
    Message string
}

func (e *ValidationError) Error() string {
    return fmt.Sprintf("验证失败 [%s]: %s", e.Field, e.Message)
}

func createUser(name string, age int) error {
    if age < 0 || age > 150 {
        return fmt.Errorf("创建用户失败: %w", &ValidationError{
            Field: "age", Message: fmt.Sprintf("无效年龄: %d", age),
        })
    }
    return nil
}

func main() {
    err := createUser("张三", -5)
    var valErr *ValidationError
    if errors.As(err, &valErr) {
        fmt.Printf("字段 %s 验证失败: %s\n", valErr.Field, valErr.Message)
        // 输出: 字段 age 验证失败: 无效年龄: -5
    }
}
```

## panic 和 recover

Go 提供了 `panic` 和 `recover` 机制来处理不可恢复的严重错误，但日常开发中应**极少使用**。

### panic 的使用场景

`panic` 用于表示程序无法继续执行的严重错误，例如：程序启动时关键配置缺失、不可能到达的代码路径等。

> **原则**：如果错误可以被调用者合理处理，就返回 `error`；只有当错误意味着程序不应该继续运行时，才使用 `panic`。

### recover 的正确用法

`recover` 用于捕获 `panic`，**必须在 `defer` 函数中调用**才有效：

```go
package main

import "fmt"

func safeDiv(a, b int) (result int, err error) {
    defer func() {
        if r := recover(); r != nil {
            err = fmt.Errorf("发生了 panic: %v", r)
        }
    }()
    if b == 0 {
        panic("除零错误")
    }
    return a / b, nil
}

func main() {
    result, err := safeDiv(10, 0)
    if err != nil {
        fmt.Println("捕获到错误:", err)
    } else {
        fmt.Println("结果:", result)
    }
}
```

### 示例：用 recover 实现安全的 HTTP Handler

在生产环境中，可以用 `recover` 防止单个请求的 panic 导致整个服务崩溃：

```go
package main

import (
    "log"
    "net/http"
)

// safeHandler 包装 handler，捕获 panic 防止服务崩溃
func safeHandler(handler func(http.ResponseWriter, *http.Request)) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if rec := recover(); rec != nil {
                log.Printf("捕获到 panic: %v, 请求: %s %s", rec, r.Method, r.URL.Path)
                http.Error(w, "服务器内部错误", http.StatusInternalServerError)
            }
        }()
        handler(w, r)
    }
}

func main() {
    // 使用 safeHandler 包装所有 handler
    http.HandleFunc("/", safeHandler(func(w http.ResponseWriter, r *http.Request) {
        // 业务逻辑...
        w.Write([]byte("OK"))
    }))
    log.Fatal(http.ListenAndServe(":8080", nil))
}
```

## 错误处理最佳实践

### 1. 尽早检查错误，在源头添加上下文

错误处理应紧跟调用点，并在包装时添加上下文信息：

```go
package main

import (
    "fmt"
    "os"
)

func readConfig(path string) ([]byte, error) {
    data, err := os.ReadFile(path)
    if err != nil {
        // 尽早检查，并添加上下文
        return nil, fmt.Errorf("读取配置文件 %s 失败: %w", path, err)
    }
    return data, nil
}

func main() {
    _, err := readConfig("/etc/myapp/config.yaml")
    if err != nil {
        fmt.Println(err)
    }
}
```

### 2. 不要忽略错误

```go
// 不好的做法：忽略错误
result, _ := doSomething()

// 好的做法：至少记录错误
result, err := doSomething()
if err != nil {
    log.Printf("警告: 操作失败: %v", err)
}
```

### 3. 只处理一次错误，在边界层统一记录

不要在中间层重复记录错误日志。建议在**边界层**（如 main 函数、HTTP handler）统一记录，中间层只负责包装和传递：

```go
// 中间层：只包装，不记录
func process() error {
    err := doStep1()
    if err != nil {
        return fmt.Errorf("step1: %w", err)
    }
    return nil
}

// 边界层：统一记录
func main() {
    if err := process(); err != nil {
        log.Println(err)
    }
}
```

### 4. 定义哨兵错误

对于常见的错误类型，使用包级别的哨兵错误变量：

```go
// 定义哨兵错误
var (
    ErrEmptyInput   = errors.New("输入为空")
    ErrInvalidFormat = errors.New("格式无效")
)

func process(input string) error {
    if input == "" {
        return fmt.Errorf("处理数据: %w", ErrEmptyInput)
    }
    return nil
}

// 调用方用 errors.Is 判断
err := process("")
if errors.Is(err, ErrEmptyInput) {
    fmt.Println("请提供输入数据")
}
```

## 总结

本篇系统介绍了 Go 的错误处理机制。Go 通过显式的 `error` 返回值而非异常来处理错误，让程序流程更加清晰。`errors.Is` 和 `errors.As` 配合 `%w` 错误包装，构成了完整的错误链处理能力。`panic/recover` 仅用于不可恢复的严重场景。遵循"尽早检查、添加上下文、在边界层统一处理"等最佳实践，可以写出健壮且可维护的错误处理代码。

