Go语言错误处理

Go语言没有类似Java的异常机制,而是采用显式的错误返回值来处理错误。这种方式鼓励开发者显式地检查和处理每个可能出错的操作,代码更加健壮和可预测。核心是内置的error接口,开发者可以自定义错误类型,并在函数中返回错误值。

设计哲学:错误就是普通的值,通过返回值传递,迫使调用方处理错误。这避免了异常流程隐藏控制逻辑,使程序的控制流更清晰。

1. error 接口

error是Go内置的接口类型,只有一个方法:Error() string。任何实现了该方法的类型都可以作为错误使用。

error接口定义
type error interface {
    Error() string
}

函数通常返回error作为最后一个返回值,调用方通过检查err != nil判断是否出错。

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

func main() {
    result, err := divide(10, 0)
    if err != nil {
        fmt.Println("错误:", err)
        return
    }
    fmt.Println("结果:", result)
}

2. 创建错误的方法

2.1 errors.New

import "errors"
err := errors.New("自定义错误信息")

2.2 fmt.Errorf

支持格式化错误信息,并提供%w动词用于错误包装(后面讲解)。

import "fmt"
err := fmt.Errorf("用户 %s 不存在,ID: %d", username, id)

3. 自定义错误类型

可以定义结构体实现error接口,携带额外上下文信息。

自定义错误结构体
type ValidationError struct {
    Field string
    Value interface{}
    Msg   string
}

func (e ValidationError) Error() string {
    return fmt.Sprintf("验证失败: %s=%v (%s)", e.Field, e.Value, e.Msg)
}

func validateAge(age int) error {
    if age < 0 {
        return ValidationError{Field: "age", Value: age, Msg: "年龄不能为负数"}
    }
    if age > 150 {
        return ValidationError{Field: "age", Value: age, Msg: "年龄过大"}
    }
    return nil
}

func main() {
    err := validateAge(-5)
    if err != nil {
        fmt.Println(err)
        // 可以类型断言获取额外字段
        if vErr, ok := err.(ValidationError); ok {
            fmt.Printf("问题字段: %s\n", vErr.Field)
        }
    }
}

4. 错误包装(Go 1.13+)

使用fmt.Errorf配合%w动词可以将一个错误包装到另一个错误中,形成错误链,便于保留原始错误信息。

func readConfigFile(path string) error {
    _, err := os.Open(path)
    if err != nil {
        // 包装原始错误
        return fmt.Errorf("打开配置文件失败: %w", err)
    }
    return nil
}

func main() {
    err := readConfigFile("/nonexist.conf")
    if err != nil {
        fmt.Println(err) // 打开配置文件失败: open /nonexist.conf: no such file or directory
    }
}

5. 错误解包与判断

标准库errors包提供了两个函数来检查错误链:

  • errors.Is(err, target) —— 判断错误链中是否包含特定错误(值比较)。
  • errors.As(err, target) —— 判断错误链中是否有类型匹配的错误,并将第一个匹配的类型赋值给目标。
使用 errors.Is
var ErrNotFound = errors.New("not found")

func findUser(id int) error {
    if id != 1 {
        return fmt.Errorf("查找用户失败: %w", ErrNotFound)
    }
    return nil
}

func main() {
    err := findUser(2)
    if errors.Is(err, ErrNotFound) {
        fmt.Println("用户不存在(通过Is检测)")
    }
}
// 使用 errors.As 获取自定义错误类型
var valErr ValidationError
if errors.As(err, &valErr) {
    fmt.Printf("自定义错误字段: %s\n", valErr.Field)
}

6. panic 与 recover

panic用于不可恢复的错误(如程序逻辑错误、数组越界等),会导致程序崩溃。在极少情况下,可以使用recover捕获panic,通常只在顶层goroutine中使用。

panic/recover 示例
func safeDivide(a, b int) {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("捕获到panic:", r)
        }
    }()
    if b == 0 {
        panic("除零错误")
    }
    fmt.Println(a / b)
}

func main() {
    safeDivide(10, 2) // 5
    safeDivide(4, 0)  // 捕获到panic: 除零错误
    fmt.Println("程序继续运行")
}
注意:不要滥用panic。错误处理应优先使用返回error,只在程序无法继续运行的情况下(如初始化失败)使用panic。

7. 错误处理最佳实践

  • 总是检查错误,不要使用_忽略错误(除非明确无关紧要)。
  • 错误信息应该清晰且可操作,避免大写开头、标点结尾(风格建议)。
  • 在调用链中使用%w包装错误,保留堆栈线索,方便定位问题。
  • 定义包级别的错误变量(如var ErrNotFound = errors.New("not found")),供外部判断。
  • 只在程序无法继续(如初始化失败)或用于测试时使用panic。
  • 使用defer+recover保护goroutine,避免单个panic导致整个服务崩溃。

综合示例:带错误链的配置文件读取器

演示错误包装与解包
package main

import (
    "errors"
    "fmt"
    "os"
)

var ErrConfigMissing = errors.New("配置文件缺失")

func readFile(path string) error {
    _, err := os.ReadFile(path)
    if err != nil {
        return fmt.Errorf("读取文件失败 %s: %w", path, err)
    }
    return nil
}

func loadConfig(path string) error {
    if path == "" {
        return fmt.Errorf("无效的配置路径: %w", ErrConfigMissing)
    }
    if err := readFile(path); err != nil {
        return fmt.Errorf("加载配置失败: %w", err)
    }
    return nil
}

func main() {
    err := loadConfig("/not/exist.conf")
    if err != nil {
        fmt.Println("错误详情:", err)

        // 判断是否为 ErrConfigMissing
        if errors.Is(err, ErrConfigMissing) {
            fmt.Println("根因:配置路径缺失")
        }

        // 获取最底层的 os.PathError
        var pathErr *os.PathError
        if errors.As(err, &pathErr) {
            fmt.Printf("底层文件错误: %s\n", pathErr.Err)
        }
    }
}
错误详情: 加载配置失败: 读取文件失败 /not/exist.conf: open /not/exist.conf: no such file or directory
底层文件错误: no such file or directory
小结:Go语言通过error接口和函数返回值实现了简单而强大的错误处理。开发者应养成良好的错误检查习惯,使用包装和解包函数构建清晰的错误链。下一章我们将学习Go并发模型的核心——goroutine和channel。