目录

Go 语言错误处理

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

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 创建一个包含固定消息的错误:

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

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)
    }
}

自定义错误类型

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

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 用于判断错误链中是否包含某个特定的目标错误,类似于 == 但能处理错误包装:

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 动词,可以在错误消息中包装另一个错误,形成错误链:

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 用于从错误链中提取特定类型的错误,类似于类型断言但能穿透包装:

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 提供了 panicrecover 机制来处理不可恢复的严重错误,但日常开发中应极少使用

panic 的使用场景

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

原则:如果错误可以被调用者合理处理,就返回 error;只有当错误意味着程序不应该继续运行时,才使用 panic

recover 的正确用法

recover 用于捕获 panic必须在 defer 函数中调用才有效:

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 导致整个服务崩溃:

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. 尽早检查错误,在源头添加上下文

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

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. 不要忽略错误

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

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

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

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

// 中间层:只包装,不记录
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. 定义哨兵错误

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

// 定义哨兵错误
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.Iserrors.As 配合 %w 错误包装,构成了完整的错误链处理能力。panic/recover 仅用于不可恢复的严重场景。遵循"尽早检查、添加上下文、在边界层统一处理"等最佳实践,可以写出健壮且可维护的错误处理代码。