错误处理基础

27.1 Go 的错误处理哲学

Go 语言采用了一种与众不同的错误处理方式。与其他语言使用 try-catch 异常机制不同,Go 使用返回值来表示错误,这使得错误处理成为代码逻辑的一部分。

Go 错误处理的特点

  1. 显式处理:错误作为返回值,必须显式处理
  2. 简单直接:没有复杂的异常层级
  3. 可组合:错误可以包装和传递
  4. 可控性:调用者决定如何处理错误

为什么不用异常

// Java 风格的异常处理
try {
    file = openFile("test.txt");
    content = readFile(file);
    process(content);
} catch (FileNotFoundException e) {
    // 处理文件不存在
} catch (IOException e) {
    // 处理 IO 错误
}

// Go 风格的错误处理
file, err := os.Open("test.txt")
if err != nil {
    // 处理错误
    return err
}
content, err := io.ReadAll(file)
if err != nil {
    return err
}
process(content)

Go 的方式虽然看起来更冗长,但它让错误处理变得非常清晰和可控。

27.2 error 接口

Go 中的错误是通过 error 接口表示的:

type error interface {
    Error() string
}

任何实现了 Error() string 方法的类型都可以作为错误使用。

基本使用

package main

import (
    "errors"
    "fmt"
)

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

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

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

nil 表示成功

在 Go 中,惯例是:如果错误为 nil,表示操作成功;如果错误不为 nil,表示操作失败。

package main

import (
    "fmt"
    "os"
)

func main() {
    file, err := os.Open("nonexistent.txt")
    if err != nil {
        fmt.Println("打开文件失败:", err)
        return
    }
    defer file.Close()

    fmt.Println("文件打开成功")
}

27.3 创建错误

使用 errors.New

errors.New 是最简单的创建错误的方式:

package main

import (
    "errors"
    "fmt"
)

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

func main() {
    if err := checkAge(-5); err != nil {
        fmt.Println(err)
    }

    if err := checkAge(200); err != nil {
        fmt.Println(err)
    }
}

使用 fmt.Errorf

fmt.Errorf 可以创建格式化的错误信息:

package main

import (
    "fmt"
)

func checkName(name string) error {
    if len(name) == 0 {
        return fmt.Errorf("名字不能为空")
    }
    if len(name) > 50 {
        return fmt.Errorf("名字长度 %d 超过限制 50", len(name))
    }
    return nil
}

func main() {
    if err := checkName(""); err != nil {
        fmt.Println(err)
    }

    if err := checkName("这是一个非常非常非常非常非常非常非常非常非常非常非常非常非常非常非常非常长的名字"); err != nil {
        fmt.Println(err)
    }
}

自定义错误类型

你可以创建自己的错误类型,实现 error 接口:

package main

import "fmt"

// 自定义错误类型
type ValidationError struct {
    Field   string
    Message string
}

// 实现 error 接口
func (e *ValidationError) Error() string {
    return fmt.Sprintf("验证错误 [%s]: %s", e.Field, e.Message)
}

func validateUser(name string, age int) error {
    if len(name) == 0 {
        return &ValidationError{
            Field:   "name",
            Message: "名字不能为空",
        }
    }
    if age < 0 || age > 150 {
        return &ValidationError{
            Field:   "age",
            Message: "年龄必须在 0-150 之间",
        }
    }
    return nil
}

func main() {
    if err := validateUser("", 25); err != nil {
        fmt.Println(err)
    }

    if err := validateUser("Alice", -5); err != nil {
        fmt.Println(err)
    }
}

带更多信息的错误类型

package main

import "fmt"

type DatabaseError struct {
    Operation string
    Table     string
    Err       error
}

func (e *DatabaseError) Error() string {
    return fmt.Sprintf("数据库错误: 操作=%s, 表=%s, 原因=%v", e.Operation, e.Table, e.Err)
}

func (e *DatabaseError) Unwrap() error {
    return e.Err
}

func queryUser(id int) error {
    return &DatabaseError{
        Operation: "SELECT",
        Table:     "users",
        Err:       fmt.Errorf("连接超时"),
    }
}

func main() {
    if err := queryUser(1); err != nil {
        fmt.Println(err)
    }
}

27.4 错误检查

直接比较

使用 == 比较错误:

package main

import (
    "errors"
    "fmt"
)

var ErrNotFound = errors.New("记录不存在")

func findUser(id int) error {
    if id <= 0 {
        return ErrNotFound
    }
    return nil
}

func main() {
    if err := findUser(0); err != nil {
        if err == ErrNotFound {
            fmt.Println("用户不存在,请检查 ID")
        } else {
            fmt.Println("其他错误:", err)
        }
    }
}

使用 errors.Is

Go 1.13 引入了 errors.Is,用于错误链比较:

package main

import (
    "errors"
    "fmt"
)

var (
    ErrNotFound = errors.New("记录不存在")
    ErrTimeout  = errors.New("操作超时")
)

func queryDatabase() error {
    return fmt.Errorf("查询失败: %w", ErrTimeout)
}

func main() {
    err := queryDatabase()
    if errors.Is(err, ErrTimeout) {
        fmt.Println("是超时错误")
    }
    if errors.Is(err, ErrNotFound) {
        fmt.Println("是未找到错误")
    }
}

使用 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 validateInput(input string) error {
    if len(input) == 0 {
        return &ValidationError{
            Field:   "input",
            Message: "输入不能为空",
        }
    }
    return nil
}

func main() {
    err := validateInput("")
    if err != nil {
        var validationErr *ValidationError
        if errors.As(err, &validationErr) {
            fmt.Printf("字段 %s 验证失败: %s\n", validationErr.Field, validationErr.Message)
        } else {
            fmt.Println("其他错误:", err)
        }
    }
}

类型断言

也可以使用类型断言检查错误类型:

package main

import (
    "fmt"
    "net"
)

func main() {
    _, err := net.LookupHost("nonexistent.invalid")
    if err != nil {
        // 使用类型断言
        if dnsErr, ok := err.(*net.DNSError); ok {
            fmt.Printf("DNS 错误: %s\n", dnsErr.Name)
            if dnsErr.IsTimeout {
                fmt.Println("是超时错误")
            }
            if dnsErr.IsNotFound {
                fmt.Println("主机不存在")
            }
        } else {
            fmt.Println("其他错误:", err)
        }
    }
}

27.5 错误包装与解包

使用 %w 包装错误

Go 1.13 引入了错误包装机制:

package main

import (
    "errors"
    "fmt"
)

func readFile(filename string) error {
    return errors.New("文件不存在")
}

func processFile(filename string) error {
    err := readFile(filename)
    if err != nil {
        return fmt.Errorf("处理文件 %s 失败: %w", filename, err)
    }
    return nil
}

func main() {
    err := processFile("test.txt")
    if err != nil {
        fmt.Println("错误:", err)

        // 解包获取原始错误
        unwrapped := errors.Unwrap(err)
        if unwrapped != nil {
            fmt.Println("原始错误:", unwrapped)
        }
    }
}

错误链

错误可以多层包装,形成错误链:

package main

import (
    "errors"
    "fmt"
)

func level3() error {
    return errors.New("底层错误")
}

func level2() error {
    err := level3()
    if err != nil {
        return fmt.Errorf("level2: %w", err)
    }
    return nil
}

func level1() error {
    err := level2()
    if err != nil {
        return fmt.Errorf("level1: %w", err)
    }
    return nil
}

func main() {
    err := level1()
    if err != nil {
        fmt.Println("完整错误:", err)

        // 逐层解包
        for err != nil {
            fmt.Printf("  - %v\n", err)
            err = errors.Unwrap(err)
        }
    }
}

使用 errors.Is 遍历错误链

package main

import (
    "errors"
    "fmt"
)

var ErrPermission = errors.New("权限不足")

func checkPermission() error {
    return ErrPermission
}

func operation() error {
    err := checkPermission()
    if err != nil {
        return fmt.Errorf("操作失败: %w", err)
    }
    return nil
}

func main() {
    err := operation()
    if errors.Is(err, ErrPermission) {
        fmt.Println("检测到权限错误")
    }
}

27.6 panic 和 recover

panic

panic 用于不可恢复的错误,会导致程序立即停止:

package main

import "fmt"

func main() {
    fmt.Println("程序开始")

    panic("发生严重错误!")

    fmt.Println("这行不会执行")
}

何时使用 panic

panic 应该只在真正不可恢复的情况下使用:

  • 程序初始化失败
  • 不可恢复的状态错误
  • 空指针解引用(运行时会自动 panic)
package main

import "fmt"

func mustCompile(pattern string) {
    if pattern == "" {
        panic("正则表达式不能为空")
    }
    fmt.Println("编译成功:", pattern)
}

func main() {
    mustCompile("") // 会 panic
}

recover

recover 用于捕获 panic,只能在 defer 中使用:

package main

import "fmt"

func mayPanic() {
    panic("出问题了!")
}

func safeCall() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("捕获到 panic:", r)
        }
    }()

    mayPanic()
    fmt.Println("这行不会执行")
}

func main() {
    safeCall()
    fmt.Println("程序继续运行")
}

recover 的实际应用

package main

import (
    "fmt"
    "http"
)

func middleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if err := recover(); err != nil {
                fmt.Printf("捕获 panic: %v\n", err)
                http.Error(w, "内部服务器错误", http.StatusInternalServerError)
            }
        }()
        next.ServeHTTP(w, r)
    })
}

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        panic("出错了!")
    })

    fmt.Println("服务器启动在 :8080")
    http.ListenAndServe(":8080", nil)
}

27.7 错误处理最佳实践

1. 及早返回

// 不好的做法
func process(data string) error {
    var err error
    if len(data) > 0 {
        if data[0] == 'A' {
            // 处理逻辑
        } else {
            err = errors.New("无效数据")
        }
    } else {
        err = errors.New("空数据")
    }
    return err
}

// 好的做法
func process(data string) error {
    if len(data) == 0 {
        return errors.New("空数据")
    }
    if data[0] != 'A' {
        return errors.New("无效数据")
    }
    // 处理逻辑
    return nil
}

2. 提供有意义的错误信息

// 不好的做法
return errors.New("错误")

// 好的做法
return fmt.Errorf("用户 %d 不存在", userID)

3. 不要忽略错误

// 不好的做法
file, _ := os.Open("config.txt")

// 好的做法
file, err := os.Open("config.txt")
if err != nil {
    return fmt.Errorf("打开配置文件失败: %w", err)
}

4. 使用哨兵错误

package main

import (
    "errors"
    "fmt"
)

var (
    ErrNotFound     = errors.New("未找到")
    ErrUnauthorized = errors.New("未授权")
    ErrBadRequest   = errors.New("请求错误")
)

func getResource(id int) error {
    if id <= 0 {
        return ErrNotFound
    }
    return nil
}

func main() {
    err := getResource(0)
    if errors.Is(err, ErrNotFound) {
        fmt.Println("资源不存在")
    }
}

5. 错误只处理一次

// 不好的做法:重复记录错误
func process() error {
    err := doSomething()
    if err != nil {
        log.Println("错误:", err) // 第一次处理
        return err
    }
    return nil
}

func main() {
    if err := process(); err != nil {
        log.Println("错误:", err) // 第二次处理
    }
}

// 好的做法:只在一处处理
func process() error {
    err := doSomething()
    if err != nil {
        return fmt.Errorf("处理失败: %w", err)
    }
    return nil
}

func main() {
    if err := process(); err != nil {
        log.Println("错误:", err) // 只在这里处理
    }
}

27.8 小结

本章介绍了 Go 语言错误处理的基础知识:

  1. error 接口:Go 的错误是通过 error 接口表示的
  2. 创建错误:使用 errors.Newfmt.Errorf
  3. 自定义错误:实现 Error() 方法创建自己的错误类型
  4. 错误检查:使用 ==errors.Iserrors.As
  5. 错误包装:使用 %w 包装错误,形成错误链
  6. panic/recover:用于不可恢复的错误和恢复机制

Go 的错误处理哲学是:错误是代码逻辑的一部分,应该显式处理。这种方式虽然看起来冗长,但让代码更加清晰和可维护。在下一章中,我们将学习更多关于自定义错误和错误包装的高级用法。