三十的博客

Go 语言错误处理

本文内容基于 AI 生成结果整理,可能包含不准确信息,仅供参考使用。
发布时间
阅读量 加载中...

本文目标

与 PHP 对比

PHP 的错误处理

PHP 提供了多种错误处理机制:

  1. 错误报告: 通过 error_reporting 设置
  2. 异常处理: try-catch 机制
  3. 自定义错误处理: set_error_handler()
  4. 致命错误: 无法捕获的错误

PHP 错误处理示例

php
<?php
function divide($a, $b) {
    if ($b == 0) {
        throw new Exception("Division by zero");
    }
    return $a / $b;
}

try {
    $result = divide(10, 0);
    echo "Result: " . $result;
} catch (Exception $e) {
    echo "Error: " . $e->getMessage();
}
?>

PHP 错误处理特点

  1. 异常机制: 使用 try-catch
  2. 错误级别: E_NOTICE, E_WARNING, E_ERROR
  3. 灵活性: 可以混合使用错误和异常
  4. 自动堆栈跟踪: 异常包含调用堆栈信息

Go 的错误处理

Go 语言采用显式错误处理机制,主要特点包括:

  1. 错误作为返回值: Go 函数通常返回一个值和一个错误对象
  2. 显式检查: 调用者必须显式检查错误
  3. 无异常机制: Go 没有 try-catch 这样的异常处理机制

Go 错误处理示例

go
package main

import (
	"errors"
	"fmt"
	"os"
)

func divide(a, b float64) (float64, error) {
	if b == 0 {
		return 0, errors.New("division by zero")
	}
	return a / b, nil
}

func main() {
	result, err := divide(10, 0)
	if err != nil {
		fmt.Println("Error:", err)
		os.Exit(1)
	}
	fmt.Println("Result:", result)
}

Go 错误处理特点

  1. 多返回值: 函数可以返回结果和错误
  2. error 接口: 错误是一个实现了 error 接口的值
  3. 必须处理: 编译器不会强制,但惯例是必须检查错误
  4. 简单直接: 没有堆栈跟踪,需要手动添加

主要区别

特性 Go PHP
错误表示 实现了 error 接口的值 Exception 对象
处理方式 显式检查返回值 try-catch 机制
强制检查 惯例强制,语言不强制 不捕获异常会导致脚本终止
错误传播 通过返回值链式传播 通过抛出异常向上传播
性能影响 几乎没有性能开销 异常处理有性能开销
堆栈跟踪 需要手动添加 自动包含在异常对象中
错误类型 单一 error 类型 多种错误级别和异常类型

正确使用错误处理

基本错误处理模式

Go 中最基本的错误处理模式是通过返回 error 类型值:

go
func DoSomething() error {
    // 执行操作
    if err := someOperation(); err != nil {
        return err // 返回错误
    }
    return nil // 没有错误
}

调用方需要检查错误:

go
if err := DoSomething(); err != nil {
    // 处理错误
}

创建自定义错误

使用 errors.New

go
import "errors"

func Divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, errors.New("division by zero")
    }
    return a / b, nil
}

使用 fmt.Errorf

go
func ProcessFile(filename string) error {
    if !fileExists(filename) {
        return fmt.Errorf("file %s does not exist", filename)
    }
    // ...
}

错误包装与解包

Go 1.13 引入了错误包装机制:

go
import "errors"

func ReadConfig() error {
    if err := readFile(); err != nil {
        return fmt.Errorf("config read failed: %w", err)
    }
    return nil
}

// 使用
err := ReadConfig()
if errors.Is(err, os.ErrNotExist) {
    // 处理文件不存在的特定情况
}

最佳实践

尽早处理错误

go
func Process() error {
    if err := step1(); err != nil {
        return err
    }
    if err := step2(); err != nil {
        return err
    }
    return step3()
}

提供有意义的错误信息

go
// 不好
return errors.New("invalid input")

// 更好
return fmt.Errorf("invalid input: expected number between 1-100, got %d", input)

区分错误类型

go
var (
    ErrInvalidInput = errors.New("invalid input")
    ErrNotFound     = errors.New("not found")
)

func FindUser(id string) (*User, error) {
    if id == "" {
        return nil, ErrInvalidInput
    }
    // ...
    return nil, ErrNotFound
}

高级错误处理模式

错误类型断言

go
type NotFoundError struct {
    Name string
}

func (e *NotFoundError) Error() string {
    return fmt.Sprintf("%s not found", e.Name)
}

func Find(name string) error {
    return &NotFoundError{Name: name}
}

// 使用
err := Find("example")
if notFound, ok := err.(*NotFoundError); ok {
    fmt.Println(notFound.Name, "was not found")
}

错误处理中间件

在 Web 开发中可以使用中间件统一处理错误:

go
func ErrorHandler(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if err := recover(); err != nil {
                log.Printf("panic: %v", err)
                http.Error(w, "Internal Server Error", http.StatusInternalServerError)
            }
        }()

        if err := next.ServeHTTP(w, r); err != nil {
            switch e := err.(type) {
            case *NotFoundError:
                http.Error(w, e.Error(), http.StatusNotFound)
            default:
                http.Error(w, e.Error(), http.StatusInternalServerError)
            }
        }
    })
}

实际项目中的错误处理策略

  1. 分层处理:在底层返回原始错误,在高层添加上下文
  2. 日志记录:在适当的层级记录错误
  3. 错误分类:区分业务错误和系统错误
  4. 错误恢复:在关键位置使用 recover 处理 panic
go
func BusinessLogic() (result interface{}, err error) {
    defer func() {
        if r := recover(); r != nil {
            err = fmt.Errorf("panic recovered: %v", r)
        }
    }()

    // 业务逻辑
    if err := validateInput(); err != nil {
        return nil, fmt.Errorf("validation failed: %w", err)
    }

    // ...
}

避免的错误处理反模式

忽略错误

go
_ = DoSomething() // 不好

过度泛化的错误

go
return errors.New("something went wrong") // 没有帮助

多层嵌套的错误检查

go
if err1 := step1(); err1 == nil {
    if err2 := step2(); err2 == nil {
        // 难以阅读
    }
}

错误包装详解

错误包装(Error Wrapping)是 Go 1.13 引入的一个重要特性,它允许我们在处理错误时保留原始错误的上下文信息,同时添加新的上下文信息。这使得错误跟踪和调试变得更加容易。

错误包装的基本概念

错误包装的核心思想是:

  1. 保留原始错误:不丢失底层错误的详细信息

  2. 添加上下文:在错误传递过程中可以添加更多有用的上下文信息

  3. 支持错误链检查:可以检查错误链中是否包含特定类型的错误

错误包装的 Demo 示例

go
package main

import (
	"errors"
	"fmt"
	"os"
)

func readFile(path string) error {
	_, err := os.ReadFile(path)
	if err != nil {
		// 使用%w动词包装原始错误
		return fmt.Errorf("读取文件失败: %w", err)
	}
	return nil
}

func processConfig() error {
	err := readFile("config.json")
	if err != nil {
		// 再次包装错误,添加更多上下文
		return fmt.Errorf("处理配置文件时出错: %w", err)
	}
	return nil
}

func main() {
	err := processConfig()
	if err != nil {
		fmt.Println("最终错误:", err)

		// 检查错误链中是否包含特定错误
		if errors.Is(err, os.ErrNotExist) {
			fmt.Println("根本原因: 文件不存在")
		}
	}
}

自定义错误类型的包装

go
package main

import (
	"errors"
	"fmt"
)

type ConfigError struct {
	Key   string
	Cause error
}

func (e *ConfigError) Error() string {
	return fmt.Sprintf("配置错误: 键 '%s' - %v", e.Key, e.Cause)
}

func (e *ConfigError) Unwrap() error {
	return e.Cause
}

func loadConfig(key string) error {
	// 模拟一个底层错误
	baseErr := errors.New("无效的配置值")

	// 包装自定义错误
	return &ConfigError{
		Key:   key,
		Cause: baseErr,
	}
}

func main() {
	err := loadConfig("timeout")
	if err != nil {
		fmt.Println("错误:", err)

		var configErr *ConfigError
		if errors.As(err, &configErr) {
			fmt.Printf("配置键 '%s' 出错,原因: %v\n",
				configErr.Key, configErr.Cause)
		}
	}
}

关键函数解析

fmt.Errorf 与 %w 动词

%w 只能作为最后一个参数使用,它会将原始错误包装到新错误中。

go
// 使用%w包装错误
wrappedErr := fmt.Errorf("上下文信息: %w", originalErr)

errors.Is

errors.Is 会检查错误链中是否包含指定的错误值。

go
if errors.Is(err, os.ErrNotExist) {
	// 处理文件不存在的特定情况
}

errors.As

errors.As 会检查错误链中是否有可以赋值给目标类型的错误,如果有则赋值。

go
var configErr *ConfigError
if errors.As(err, &configErr) {
	// 可以访问configErr的字段
}

实际应用场景

数据库操作 ​

go
func GetUser(id int) (*User, error) {
    var user User
    err := db.QueryRow("SELECT ...", id).Scan(&user)
    if err != nil {
        return nil, fmt.Errorf("查询用户 %d 失败: %w", id, err)
    }
    return &user, nil
}

API 调用

go
func CallAPI(url string) error {
    resp, err := http.Get(url)
    if err != nil {
        return fmt.Errorf("API调用失败: %w", err)
    }
    defer resp.Body.Close()
    // ...
}

配置加载

go
func LoadConfig() (*Config, error) {
    data, err := os.ReadFile("config.json")
    if err != nil {
        return nil, fmt.Errorf("加载配置文件失败: %w", err)
    }
    // ...
}

错误包装的优势

  1. 保留完整错误链:可以追溯到最初引发错误的位置
  2. 支持精确的错误检查:可以检查错误链中特定类型的错误
  3. 添加上下文信息:在错误传递过程中不断丰富错误信息
  4. 结构化错误处理:可以构建分层的、结构化的错误信息

推荐使用错误包装的场景

跨层级调用链中的错误传递

保留了完整的错误链,调试时可以清晰看到错误传播路径

go
// 数据访问层
func (r *UserRepo) GetUser(id int) (*User, error) {
    err := r.db.QueryRow("SELECT...", id).Scan(...)
    if err != nil {
        return nil, fmt.Errorf("用户数据库查询失败[id=%d]: %w", id, err)
    }
    // ...
}

// 业务逻辑层
func (s *UserService) GetUserProfile(id int) (*Profile, error) {
    user, err := s.repo.GetUser(id)
    if err != nil {
        return nil, fmt.Errorf("获取用户资料失败: %w", err)
    }
    // ...
}

需要保留原始错误进行特定检查

go
if err := processFile(); err != nil {
    if errors.Is(err, os.ErrNotExist) {
        // 特殊处理文件不存在的场景
        return createDefaultFile()
    }
    return fmt.Errorf("处理文件失败: %w", err)
}

需要添加有价值的上下文信息

go
func LoadConfig(path string) (*Config, error) {
    data, err := os.ReadFile(path)
    if err != nil {
        return nil, fmt.Errorf("加载配置文件[%s]失败: %w", path, err)
    }
    // ...
}

需要谨慎使用的场景

性能敏感的代码路径

错误包装会创建新的错误对象,在极端性能敏感的场景(如高频循环)中,简单的错误返回可能更合适:

go
// 高频调用的内部函数
func validateInput(input string) error {
    if input == "" {
        return errors.New("输入不能为空") // 直接返回简单错误
    }
    // ...
}

已经包含足够信息的错误

如果原始错误已经包含足够调试信息,不需要额外包装:

go
// 不必要包装
return fmt.Errorf("操作失败: %w", fmt.Errorf("用户 %s 不存在", username))

// 更简洁的方式
return fmt.Errorf("用户 %s 不存在", username)

顶层边界处的错误

在应用程序边界(如 HTTP handler、gRPC 方法等),通常需要将内部错误转换为对外的 API 错误,此时可能不需要保留原始错误:

go
func (h *Handler) GetUser(w http.ResponseWriter, r *http.Request) {
    user, err := h.service.GetUser(...)
    if err != nil {
        // 转换为API错误响应,不暴露内部错误细节
        render.Error(w, r, "用户不存在", http.StatusNotFound)
        return
    }
    // ...
}

使用错误包装的最佳实践

  1. 分层包装​:在每一层添加有意义的上下文,但避免过度包装
go
// 数据层
return fmt.Errorf("DB查询失败: %w", err)

// 服务层
return fmt.Errorf("获取用户数据失败: %w", err)

// 不推荐这样
return fmt.Errorf("错误: %w", fmt.Errorf("失败: %w", fmt.Errorf("问题: %w", err)))
  1. ​ 保持错误信息有用
go
// 好 - 包含操作和关键参数
return fmt.Errorf("发送邮件到%s失败: %w", email, err)

// 不好 - 信息太泛
return fmt.Errorf("操作失败: %w", err)
  1. 统一错误处理风格
  1. 配合日志记录 ​
go
if err := process(); err != nil {
    log.Printf("处理失败: %+v", err) // %+v可以打印完整错误链
    return fmt.Errorf("处理操作失败")
}
#错误处理 #Golang