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 操作包含两个步骤:
- 将返回值赋值(若返回值有名字,直接赋值;否则创建临时变量)。
- 执行 defer 链。
- 真正返回。
因此 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)
}
核心要点速记:
- error:实现了
Error() string的接口,用errors.New/fmt.Errorf创建,%w包装。 - defer:函数返回前执行,LIFO 顺序,参数立即求值,可用于资源释放和修改命名返回值。
- panic:引发运行时恐慌,沿调用栈向上传播,执行 defer 后崩溃。
- recover:仅在 defer 内有效,捕获 panic 值,恢复执行避免崩溃。
- 使用区分:可预期的错误用
error;不可恢复的致命 bug 用panic;顶层或 goroutine 入口用recover防止雪崩。
浙公网安备 33010602011771号