7、错误处理、defer、panic、recover

7、错误处理、defer、panic/recover


错误处理(Error Handling)

error 接口

Go 语言使用 error 接口来表示错误状态,内置定义如下:

type error interface {
    Error() string
}

任何实现了 Error() string 方法的类型,都是一个错误。

创建错误

  • errors.New:创建简单的字符串错误。
err := errors.New("something went wrong")
  • fmt.Errorf:格式化错误信息,并支持用 %w 包装另一个错误。
err := fmt.Errorf("open file failed: %w", underlyingErr)

错误的传递与检查

函数通常将 error 作为最后一个返回值:

func doSomething() (int, error) {
    if fail {
        return 0, errors.New("failed")
    }
    return 42, nil
}

调用方必须检查错误:

result, err := doSomething()
if err != nil {
    // 处理错误,可能返回或记录日志
    return err
}
// 使用 result

错误包装与解包(Go 1.13+)

  • 包装:使用 fmt.Errorf 配合 %w 保留原始错误。
  • 解包:errors.Unwrap(err) 逐层获取原始错误。
  • 检查:errors.Is(err, target) 判断错误链中是否包含特定错误;errors.As(err, &target) 提取特定类型的错误。
var ErrNotFound = errors.New("not found")

func find(id string) error {
    return fmt.Errorf("find user %s: %w", id, ErrNotFound)
}

err := find("123")
if errors.Is(err, ErrNotFound) {
    fmt.Println("user not found")
}

自定义错误类型

为携带更多上下文,可实现自定义错误结构体:

type ParseError struct {
    Filename string
    Line     int
    Err      error
}

func (e *ParseError) Error() string {
    return fmt.Sprintf("%s:%d: %v", e.Filename, e.Line, e.Err)
}

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

defer 延迟执行

语法与执行时机

defer 语句将其后的函数调用推迟到所在函数即将返回时执行(无论是正常返回还是 panic)。常用于资源清理、解锁、关闭文件等。

func readFile(path string) ([]byte, error) {
    f, err := os.Open(path)
    if err != nil {
        return nil, err
    }
    defer f.Close()  // 确保文件关闭

    return io.ReadAll(f)
}

多个 defer 的执行顺序

多个 defer 按 后进先出(LIFO) 顺序执行,类似于栈。

func demo() {
    defer fmt.Println("first defer")
    defer fmt.Println("second defer")
    defer fmt.Println("third defer")
    fmt.Println("function body")
}
// 输出:
// function body
// third defer
// second defer
// first defer

defer 的参数求值

defer 语句注册时,函数参数会立即求值(闭包引用变量则延迟求值)。

func paramEval() {
    i := 0
    defer fmt.Println(i) // 此时 i=0 被记录下来,输出 0
    i++
    return
}
// 输出 0

若使用闭包,则延迟求值:

defer func() { fmt.Println(i) }() // 输出 1

defer 与 return 的顺序

return 操作包含两个步骤:

  1. 将返回值赋值(若返回值有名字,直接赋值;否则创建临时变量)。
  2. 执行 defer 链。
  3. 真正返回。

因此 defer 可以修改命名返回值。

func example() (result int) {
    defer func() { result++ }()
    return 10
}
// 返回 11

panic 与 recover

panic —— 程序恐慌

panic 用于触发运行时错误,会立即停止当前函数执行,并开始沿调用栈向上传播,执行每一层的 defer,最后程序崩溃退出。

func must(i int) {
    if i < 0 {
        panic("i must be non-negative")
    }
}

recover —— 拦截 panic

recover 是 Go 内置函数,只能在 defer 函数中生效。它捕获当前 panic 的值,使程序恢复正常执行(从 panic 发生的函数之后继续执行是不可能的,但能防止程序崩溃)。

func safeExecute() (err error) {
    defer func() {
        if r := recover(); r != nil {
            err = fmt.Errorf("recovered from panic: %v", r)
        }
    }()
    panic("something terrible")
}

调用 safeExecute() 会返回一个错误,而不是崩溃。

panic 与错误处理的适用场景

场景 处理方式 说明
可预见的业务错误 返回 error 文件不存在、网络超时、用户输入无效等
不可恢复的内部逻辑错误 panic 数组越界、空指针引用(通常程序 bug)
初始化失败(如 init) panic 依赖缺失,程序无法正常启动
不应该发生的状态 panic 编写断言:代码逻辑不应到达的分支
公共库函数 不要主动 panic 应返回 error,由调用方决定如何处理
HTTP handler recover 中间件 防止单个请求 panic 导致整个进程挂掉

黄金法则:

  • 使用 error 处理预期内可能失败的操作。
  • panic 仅用于不可恢复的严重错误(bug 或无法继续运行的状况)。
  • 在顶层(如 goroutine 入口)或中间件中使用 recover 做兜底,确保服务不崩溃。

带 defer 的文件读取函数示例

package main

import (
    "fmt"
    "io"
    "os"
)

// ReadFileContent 读取文件全部内容并返回字符串。
// 若文件打开失败或读取失败,返回错误。
func ReadFileContent(filename string) (content string, err error) {
    // 打开文件
    f, err := os.Open(filename)
    if err != nil {
        return "", fmt.Errorf("open file %s: %w", filename, err)
    }
    // 使用 defer 确保文件关闭,即使发生 panic 也会执行
    defer f.Close()

    // 读取内容
    data, err := io.ReadAll(f)
    if err != nil {
        return "", fmt.Errorf("read file %s: %w", filename, err)
    }

    return string(data), nil
}

func main() {
    content, err := ReadFileContent("test.txt")
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
    fmt.Println(content)
}

核心要点速记:

  1. error:实现了 Error() string 的接口,用 errors.New / fmt.Errorf 创建,%w 包装。
  2. defer:函数返回前执行,LIFO 顺序,参数立即求值,可用于资源释放和修改命名返回值。
  3. panic:引发运行时恐慌,沿调用栈向上传播,执行 defer 后崩溃。
  4. recover:仅在 defer 内有效,捕获 panic 值,恢复执行避免崩溃。
  5. 使用区分:可预期的错误用 error;不可恢复的致命 bug 用 panic;顶层或 goroutine 入口用 recover 防止雪崩。
posted @ 2026-09-23 16:21  爱喝奶茶的CC怪  阅读(9)  评论(0)    收藏  举报