引入

程序总会遇到意外情况——文件找不到、网络断了、参数不合法。很多语言用异常(exception)来处理这些问题,但 Go 选择了另一条路:用返回值代替异常。函数多返回一个 error 值,调用方检查它是不是 nil,就知道有没有出错。这种方式让错误处理变得显式、可控,不会悄悄被忽略。

正文

定义

定义

错误处理是程序应对异常情况的方式。Go 用 error 接口表示错误,函数通过返回 error 值来告知调用方是否出错。调用方检查 error 是否为 nil 来判断是否出错,为 nil 表示成功,不为 nil 表示有错误。

语法

// error 接口定义(Go 内置)
type error interface {
    Error() string
}
 
// 创建错误
err := errors.New("something went wrong")
 
// 带格式的错误
err := fmt.Errorf("expected %d, got %d", 10, 20)
 
// 检查错误
result, err := doSomething()
if err != nil {
    // 处理错误
    return err
}

例子

标准错误检查模式

file, err := os.Open("config.txt")
if err != nil {
    fmt.Println("打开文件失败:", err)
    return
}
defer file.Close()
 
// 正常使用 file

这是 Go 里最常见的错误处理模式:先检查 err,有问题就提前返回,没问题就继续。

errors.New()

import "errors"
 
func divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, errors.New("除数不能为 0")
    }
    return a / b, nil
}
 
func main() {
    result, err := divide(10, 0)
    if err != nil {
        fmt.Println("错误:", err)
        return
    }
    fmt.Println("结果:", result)
}

errors.New() 用字符串创建一个简单的错误值,适合错误信息固定的场景。

fmt.Errorf()

import "fmt"
 
func getAge(name string) (int, error) {
    if name == "" {
        return 0, fmt.Errorf("名字不能为空")
    }
    if len(name) > 50 {
        return 0, fmt.Errorf("名字太长: %d 个字符", len(name))
    }
    return 25, nil
}

fmt.Errorf() 支持格式化字符串,可以把变量插入到错误信息里,比 errors.New() 更灵活。

自定义错误类型

type ValidationError struct {
    Field   string
    Message string
}
 
func (e *ValidationError) Error() string {
    return fmt.Sprintf("字段 %s 校验失败: %s", e.Field, e.Message)
}
 
func validateEmail(email string) error {
    if email == "" {
        return &ValidationError{
            Field:   "email",
            Message: "不能为空",
        }
    }
    return nil
}
 
func main() {
    err := validateEmail("")
    if err != nil {
        fmt.Println(err)
        // 字段 email 校验失败: 不能为空
    }
}

只要实现 Error() string 方法,任何类型都可以当 error 用。自定义错误类型可以携带更多上下文信息

判断错误类型

err := validateEmail("")
 
if err != nil {
    // 类型断言判断具体错误类型
    if ve, ok := err.(*ValidationError); ok {
        fmt.Printf("字段: %s, 原因: %s\n", ve.Field, ve.Message)
    } else {
        fmt.Println("未知错误:", err)
    }
}

用类型断言可以区分不同的错误类型,做针对性的错误处理

errors.Is() 和 errors.As()

import "errors"
 
// 预定义哨兵错误
var ErrNotFound = errors.New("not found")
 
func findUser(id int) (string, error) {
    if id <= 0 {
        return "", ErrNotFound
    }
    return "Alice", nil
}
 
func main() {
    _, err := findUser(-1)
 
    // errors.Is:判断错误链中是否包含指定错误
    if errors.Is(err, ErrNotFound) {
        fmt.Println("用户不存在")
    }
 
    // errors.As:从错误链中提取指定类型的错误
    var ve *ValidationError
    if errors.As(err, &ve) {
        fmt.Println("校验失败:", ve.Field)
    }
}

errors.Is()errors.As() 是 Go 1.13 引入的,支持错误包装链,比直接比较 == 更可靠。

包装错误

func readConfig(path string) error {
    _, err := os.Open(path)
    if err != nil {
        // %w 把原始错误包装进去
        return fmt.Errorf("读取配置失败: %w", err)
    }
    return nil
}
 
func main() {
    err := readConfig("missing.txt")
    if err != nil {
        fmt.Println(err)
        // 读取配置失败: open missing.txt: ...
 
        // 用 errors.Is 可以检查被包装的原始错误
        if errors.Is(err, os.ErrNotExist) {
            fmt.Println("文件不存在")
        }
    }
}

fmt.Errorf()%w 而不是 %v 来包装错误,保留原始错误信息,让 errors.Is()errors.As() 能正常工作。

常见写法

写法说明
errors.New("msg")创建简单错误
fmt.Errorf("msg %d", v)创建格式化错误
fmt.Errorf("msg: %w", err)包装错误(保留链)
if err != nil { return err }检查并返回错误
errors.Is(err, target)判断错误链是否包含目标错误
errors.As(err, &target)从错误链提取指定类型
自定义 Error() string自定义错误类型

特点

  • Go 没有 try-catch 异常机制,错误就是普通返回值
  • error 是一个接口,只要实现 Error() string 方法就行
  • 错误检查是显式的,必须手动 if err != nil,不会悄悄吞掉错误
  • errors.New()fmt.Errorf() 是创建错误的两种标准方式
  • %w 用于包装错误,%v 只是格式化字符串,不保留错误链
  • Early return 是 Go 错误处理的主流风格:有错就提前返回,不嵌套
  • 不要把 error 丢掉不管(_ = doSomething() 这种写法要谨慎)

理解

Go 的错误处理哲学是”错误是值,不是异常”。没有隐藏的异常传播链,每个错误都在代码里明明白白地处理。虽然 if err != nil 写多了觉得啰嗦,但好处是错误不会被忽略,代码流程一目了然。自定义错误类型配合 errors.Is()errors.As() 可以构建清晰的错误处理体系。

引出

错误处理是 Go 基础语法里最后一个大块。接下来可以进入 Go 数据类型,学习 Go 里的各种数据类型:整型、浮点型、字符串、数组、切片、map 等。