Go 函数与错误处理

AI 辅助生成本文由 AI 起草并经人工核查,示例均标明软件版本;发现错误请以官方文档为准。

Go 1.24

Go 没有异常,错误是普通的返回值。这个设计意味着:错误必须被显式处理,也意味着读代码时能一眼看到哪些地方会失败。

函数定义与多返回值

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

调用时必须处理两个返回值:

result, err := divide(10, 2)
if err != nil {
    log.Printf("计算出错:%v", err)
    return
}
fmt.Println(result)   // 5

同名类型可以合并写 a, b float64。返回值多于两个时,建议定义一个结构体,而不是继续堆返回值。

命名返回值

func parse(raw string) (n int, err error) {
    n, err = strconv.Atoi(raw)
    if err != nil {
        err = fmt.Errorf("解析 %q 失败: %w", raw, err)
    }
    return   // 裸返回,返回当前 n 和 err
}

命名返回值适合需要在 defer 里修改结果的场景,但裸返回会牺牲可读性,函数超过 20 行就不建议再用。

error 是什么

type error interface {
    Error() string
}

error 是一个只有一个方法的接口。任何实现了 Error() string 的类型都能当错误用:

type ConfigError struct {
    Path string
}

func (e *ConfigError) Error() string {
    return "配置文件不可读:" + e.Path
}

返回它时要注意:返回指针类型 &ConfigError{},不要返回值类型,否则 errors.As 匹配不到(接口里存的是值的副本)。

错误处理的三条惯例

1. 错误是最后一个返回值,nil 表示成功:

func load(path string) ([]byte, error)

2. 用 %w 包装,保留错误链:

data, err := os.ReadFile(path)
if err != nil {
    return nil, fmt.Errorf("读取配置 %s: %w", path, err)
}

%v 只保留文本,%w 保留原始错误,上层才能用 errors.Is / errors.As 判断。

3. 用 errors.Is / errors.As 判断,不要比较字符串:

if errors.Is(err, os.ErrNotExist) {
    // 文件不存在
}

var pathErr *os.PathError
if errors.As(err, &pathErr) {
    fmt.Println("出错路径:", pathErr.Path)
}

errors.Is 沿错误链逐层比较(支持自定义 Is 方法),errors.As 沿链查找指定类型。直接写 err.Error() == "..." 在包装后会失效。

错误的传递与兜底

func loadConfig(path string) (*Config, error) {
    data, err := os.ReadFile(path)
    if err != nil {
        return nil, fmt.Errorf("loadConfig: %w", err)
    }

    var cfg Config
    if err := json.Unmarshal(data, &cfg); err != nil {
        return nil, fmt.Errorf("解析 %s: %w", path, err)
    }
    return &cfg, nil
}

原则:中间层只加上下文,不吞错误。真正决定「要不要继续」的是调用链顶层:

cfg, err := loadConfig("config.json")
if err != nil {
    if errors.Is(err, os.ErrNotExist) {
        cfg = defaultConfig()   // 可恢复:用默认值
    } else {
        log.Fatal(err)          // 不可恢复:终止
    }
}

可变参数与函数作为值

func sum(nums ...int) int {
    total := 0
    for _, n := range nums {
        total += n
    }
    return total
}

sum(1, 2, 3)      // 6
nums := []int{4, 5}
sum(nums...)      // 9,展开切片

函数是一等值,可以赋值、传参、返回:

type Middleware func(http.Handler) http.Handler

func apply(h http.Handler, mws ...Middleware) http.Handler {
    for i := len(mws) - 1; i >= 0; i-- {
        h = mws[i](h)
    }
    return h
}

defer:延迟执行的清理

func readHead(path string) ([]byte, error) {
    f, err := os.Open(path)
    if err != nil {
        return nil, err
    }
    defer f.Close()   // 函数返回前执行,无论是否出错

    buf := make([]byte, 64)
    n, err := f.Read(buf)
    return buf[:n], err
}

三条规则:

  1. 多个 defer 按后进先出(LIFO)执行。
  2. 参数在 defer 语句处立即求值,不是执行时求值:
x := 1
defer fmt.Println(x)   // 打印 1,不是 2
x = 2

3 想在 defer 里读最新值,用闭包:

defer func() { fmt.Println(x) }()   // 打印 2

注意:os.Exit 与 panic 之外的「函数返回」都会执行 defer,但 os.Exit 会直接终止进程,不执行任何 defer。

panic 与 recover

panic 用于程序无法继续的情况(配置缺失、不可能的状态),不是普通错误处理手段:

func mustLoad(path string) *Config {
    cfg, err := loadConfig(path)
    if err != nil {
        panic(err)   // 立即终止,打印堆栈
    }
    return cfg
}

recover 只能在 defer 里生效,用于进程级边界兜底(如 HTTP 中间件、任务调度器):

defer func() {
    if r := recover(); r != nil {
        log.Printf("捕获 panic: %v", r)
    }
}()

经验法则:库代码不要 recover,把 panic 留给调用方;业务错误一律用 error 返回。

常见错误与注意事项

  1. 用 _ 丢弃错误:data, _ := os.ReadFile(p) 让失败静默,至少记录日志。
  2. 包装时用 %v 而不是 %w:上层 errors.Is 判断失效。
  3. 比较错误字符串:包装后文本会变,用 errors.Is / errors.As。
  4. 返回错误类型值而不是指针:errors.As 匹配不到自定义类型。
  5. defer 里的参数被提前求值:需要最新值时用闭包。
  6. 用 panic 处理可恢复错误:调用方无法按类型处理,进程直接崩。
  7. 忘记 defer f.Close():句柄泄漏,循环里打开文件时尤其危险。

小结

  • 错误是普通返回值,放最后,nil 表示成功。
  • 用 fmt.Errorf("上下文: %w", err) 包装,errors.Is / errors.As 判断。
  • 中间层只加上下文,顶层决定恢复或终止。
  • defer 后进先出,参数立即求值;os.Exit 不执行 defer。
  • panic 只用于不可继续的情况,recover 只放在进程边界。

下一步