Skip to content

错误处理

本章深入Go错误处理:error接口、错误创建与传递、错误包装、panic与recover。重点:Go没有异常机制,错误是返回值;理解fmt.Errorf包装和errors.Unwrap解包、panic仅用于真正无法恢复的错误。

前置知识:第5章 函数 学习目标:掌握Go错误处理模式、理解panic vs error的适用场景


type error interface {
Error() string
}

Go的错误只是实现Error()方法的接口。非常简单,但非常强大。

errors.New
import "errors"
err := errors.New("文件未找到")
fmt.Println(err.Error()) // "文件未找到"
// 方式2:fmt.Errorf
err := fmt.Errorf("读取文件失败: %s", filename)
import "errors"
// 标准库常用错误
var ErrPermission = errors.New("permission denied")
var ErrNotFound = errors.New("not found")
var ErrExist = errors.New("file already exists")
// 使用
if os.Open("test.txt") == nil {
// 不存在,处理错误
}

本节目的:掌握错误创建模式、错误链传递原则

func readFile(filename string) error {
file, err := os.Open(filename)
if err != nil {
return err // 简单传播
}
defer file.Close()
// 处理文件...
return nil
}
func getUser(id int) (*User, error) {
user, ok := users[id]
if !ok {
return nil, fmt.Errorf("用户ID %d 不存在", id)
}
return &user, nil
}
// 不好:每次创建新error
if err != nil {
return errors.New("用户未找到") // 无法用==比较
}
// 好:使用预定义错误变量
var ErrUserNotFound = errors.New("用户未找到")
if err == ErrUserNotFound {
// 可以精确比较
}
// 使用errors.As提取错误类型
type PathError struct {
Path string
Err error
}
func (e *PathError) Error() string {
return e.Path + ": " + e.Err.Error()
}
var perr *PathError
if errors.As(err, &perr) {
fmt.Println("路径错误:", perr.Path)
fmt.Println("原始错误:", perr.Err)
}

本节目的:理解错误包装的目的、掌握%w和errors.Unwrap的用法

原始错误丢失上下文:

// 不好:只知道"读取文件失败",不知道哪个文件
return errors.New("读取文件失败")
// 好:包含文件名和原始错误
return fmt.Errorf("读取文件 %s 失败: %w", filename, err)
import "errors"
err1 := errors.New("原始错误")
err2 := fmt.Errorf("包装1: %w", err1)
err3 := fmt.Errorf("包装2: %w", err2)
// 解包
fmt.Println(errors.Unwrap(err3)) // 包装1: 原始错误
fmt.Println(errors.Unwrap(errors.Unwrap(err3))) // 原始错误
// 判断错误链
if errors.Is(err3, err1) {
fmt.Println("err3包含err1") // true
}
func readConfig() error {
file, err := os.Open("config.json")
if err != nil {
return fmt.Errorf("打开配置文件失败: %w", err)
}
defer file.Close()
var cfg Config
if err := json.NewDecoder(file).Decode(&cfg); err != nil {
return fmt.Errorf("解析配置文件失败: %w", err)
}
return nil
}
// 调用
err := readConfig()
if err != nil {
if errors.Is(err, os.ErrNotExist) {
fmt.Println("配置文件不存在")
} else {
fmt.Println("其他错误:", err)
}
}
type ValidationError struct {
Field string
Msg string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("%s: %s", e.Field, e.Msg)
}
func (e *ValidationError) Unwrap() error {
return errors.New("validation failed")
}
// 检查错误链
var ve *ValidationError
if errors.As(err, &ve) {
fmt.Println("验证错误:", ve.Field, ve.Msg)
}

本节目的:理解panic的用途、掌握recover捕获时机

panic是Go的错误处理机制,用于处理”不应该发生”的错误:

// 触发panic
panic("发生了严重错误")
// panic后可接任何值
panic(err)
// 1. 初始化失败
func init() {
if os.Getenv("CONFIG_PATH") == "" {
panic("CONFIG_PATH must be set")
}
}
// 2. 程序员错误(不应该发生)
if obj == nil {
panic("obj is nil - this is a bug")
}
// 3. 不可恢复的错误(如数组越界是panic)
// Go会自动对运行时错误触发panic
# Python
raise ValueError("invalid value")
raise Exception("something went wrong")
func safeCall() {
// defer中的recover可以捕获panic
defer func() {
if r := recover(); r != nil {
fmt.Println("捕获panic:", r)
}
}()
fmt.Println("开始")
panic("oops")
fmt.Println("不会执行")
}
func main() {
safeCall()
fmt.Println("继续执行")
}

panic时,defer按LIFO顺序执行:

func main() {
defer fmt.Println("1")
defer fmt.Println("2")
defer fmt.Println("3")
panic("panic!")
}
// 输出:3 2 1(后进先出)
场景使用原因
函数可能失败error返回预期内的失败
文件不存在error可以处理
程序员的bugpanic不应该发生
初始化失败panic无法继续
func handle(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
log.Printf("panic: %v", err)
http.Error(w, "Internal Server Error", 500)
}
}()
// handler逻辑...
}

本节目的:掌握Go错误处理最佳实践、避免常见反模式

  1. 错误作为返回值:Go没有异常,错误通过返回值传递
  2. 不要忽略错误:至少记录或传播
  3. 添加上下文:用fmt.Errorf包装
  4. 区分错误类型:使用errors.Is/As
// 反模式1:忽略错误
file, _ := os.Open("test.txt") // 不要这样
// 反模式2:只返回字符串
return errors.New("文件未找到") // 无法分类
// 反模式3:panic用于正常错误
if err != nil {
panic(err) // 不要这样
}
// 反模式4:多层嵌套
if err != nil {
return err // 层层返回,无法定位
}
// 模式1:立即处理
data, err := os.ReadFile("test.txt")
if err != nil {
return fmt.Errorf("读取配置失败: %w", err)
}
// 模式2:操作失败时包装
func process() error {
if err := step1(); err != nil {
return fmt.Errorf("步骤1失败: %w", err)
}
if err := step2(); err != nil {
return fmt.Errorf("步骤2失败: %w", err)
}
return nil
}
// 模式3:使用错误变量
var ErrNotFound = errors.New("资源不存在")
func find(id string) (Resource, error) {
r, ok := cache.Get(id)
if !ok {
return nil, ErrNotFound
}
return r, nil
}
// 汇总多个错误
type MultiError struct {
errors []error
}
func (m *MultiError) Add(err error) {
if err != nil {
m.errors = append(m.errors, err)
}
}
func (m *MultiError) Error() string {
if len(m.errors) == 0 {
return ""
}
return fmt.Sprintf("%d errors occurred", len(m.errors))
}
// 使用
var errs MultiError
for _, task := range tasks {
if err := task.Run(); err != nil {
errs.Add(err)
}
}
if len(errs.errors) > 0 {
return &errs
}
import "log"
// 记录并返回
if err != nil {
log.Printf("读取文件失败: %v", err)
return err
}
// 使用log.Fatal记录致命错误(会退出程序)
if err != nil {
log.Fatalf("无法启动: %v", err)
}

  1. 错误检查不遗漏:每个err != nil都要处理,不要用_忽略错误
  2. 包装而非隐藏:用fmt.Errorf("context: %w", err)包装错误,保留原始错误信息
  3. panic仅用于真正异常:程序逻辑错误(如数组越界)用panic,业务错误(如文件不存在)用error
  4. defer recover捕获panic:recover只能在defer中生效,用于将panic转为可处理的error
  5. Go 1.20+用errors.Join:合并多个错误用errors.Join,不必手动实现MultiError

PythonGo
try/exceptif err != nil
raise Exceptionpanic()
except Exception as eerrors.As(err, &target)
raise fromfmt.Errorf(“%w”, err)
无errors.Is(err1, err2)
无errors.Unwrap(err)

Go错误处理哲学:

  • 错误是值,不是异常
  • 错误应该被传播和包装
  • panic用于真正不应该发生的情况

  1. 实现一个函数,返回(int, error),处理除数为零的情况
  2. 创建自定义错误类型,包含错误码和消息
  3. 用fmt.Errorf包装错误,构建三层错误链
  4. 用errors.Is检查错误链中的特定错误
  5. 实现recover捕获panic的safe函数