跳到主要内容

Go 错误处理

错误处理是 Go 语言编程的重要组成部分。Go 采用了一种独特的错误处理方式:将错误作为普通值返回,而不是使用异常机制。这种设计使得错误处理更加显式和可控。

Go 的错误处理哲学

为什么不用异常?

Go 选择不使用异常机制,而是采用返回值的方式处理错误,原因如下:

  1. 显式性:错误处理代码就在业务代码旁边,不会被忽略
  2. 可控性:调用者决定如何处理错误,而不是被强制捕获
  3. 简单性:没有 try-catch-finally 的复杂控制流
  4. 可读性:代码流程清晰,易于理解

核心原则

Go 的错误处理遵循以下原则:

  • 错误是值,可以像其他值一样传递和处理
  • 如果可能发生错误,应该显式检查
  • 错误信息应该提供足够的上下文

error 接口

Go 的错误处理基于 error 接口:

type error interface {
Error() string
}

解释:任何实现了 Error() string 方法的类型都可以作为错误使用。这个简单的接口设计使得错误处理非常灵活。

nil 表示成功

在 Go 中,约定俗成地使用 nil 表示没有错误:

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

result, err := divide(10, 2)
if err != nil {
fmt.Println("错误:", err)
return
}
fmt.Println("结果:", result)

解释:函数返回两个值,第一个是结果,第二个是错误。如果 err == nil,表示操作成功。

创建错误

errors.New

最简单的创建错误的方式:

import "errors"

func validateAge(age int) error {
if age < 0 {
return errors.New("年龄不能为负数")
}
if age > 150 {
return errors.New("年龄不能超过150")
}
return nil
}

解释errors.New 创建一个简单的错误,包含给定的错误信息。

fmt.Errorf

格式化错误信息:

import "fmt"

func fetchUser(id int) (*User, error) {
if id <= 0 {
return nil, fmt.Errorf("无效的用户ID: %d", id)
}
return &User{ID: id}, nil
}

func openFile(path string) error {
_, err := os.Open(path)
if err != nil {
return fmt.Errorf("打开文件 %s 失败: %w", path, err)
}
return nil
}

解释fmt.Errorf 支持格式化字符串,可以创建更具描述性的错误信息。使用 %w 动词可以包装其他错误(Go 1.13+)。

自定义错误类型

当需要携带更多错误信息时,可以定义自己的错误类型:

type ValidationError struct {
Field string
Message string
}

func (e *ValidationError) Error() string {
return fmt.Sprintf("验证错误 [%s]: %s", e.Field, e.Message)
}

type NotFoundError struct {
Resource string
ID int
}

func (e *NotFoundError) Error() string {
return fmt.Sprintf("%s (ID: %d) 不存在", e.Resource, e.ID)
}

func getUser(id int) (*User, error) {
if id <= 0 {
return nil, &ValidationError{
Field: "id",
Message: "必须为正整数",
}
}

user := findUser(id)
if user == nil {
return nil, &NotFoundError{
Resource: "User",
ID: id,
}
}

return user, nil
}

解释:自定义错误类型可以携带更多上下文信息,便于调用者进行错误处理。

错误处理模式

基本模式

result, err := someFunction()
if err != nil {
return err
}

错误检查优先

func processFile(path string) error {
file, err := os.Open(path)
if err != nil {
return fmt.Errorf("打开文件失败: %w", err)
}
defer file.Close()

data, err := io.ReadAll(file)
if err != nil {
return fmt.Errorf("读取文件失败: %w", err)
}

if err := processData(data); err != nil {
return fmt.Errorf("处理数据失败: %w", err)
}

return nil
}

解释:这种模式先检查错误,再处理正常逻辑,使代码流程更加清晰。

立即返回

func doWork() error {
if err := step1(); err != nil {
return fmt.Errorf("步骤1失败: %w", err)
}

if err := step2(); err != nil {
return fmt.Errorf("步骤2失败: %w", err)
}

if err := step3(); err != nil {
return fmt.Errorf("步骤3失败: %w", err)
}

return nil
}

解释:每个步骤出错后立即返回,避免深层嵌套。

忽略错误

某些情况下可以忽略错误,但要确保这是有意为之:

// 写入文件,忽略错误(例如写入临时文件)
_ = os.WriteFile("/tmp/cache", data, 0644)

// 删除文件,忽略"文件不存在"的错误
_ = os.Remove(path)

注意:忽略错误应该是有意为之,并添加注释说明原因。

错误包装与解包

Go 1.13 引入了错误包装机制,允许在错误上添加上下文信息。

包装错误

使用 %w 包装错误:

func readConfig(path string) error {
file, err := os.Open(path)
if err != nil {
return fmt.Errorf("读取配置文件失败: %w", err)
}
defer file.Close()

return nil
}

解释%w 会保留原始错误,使其可以被 errors.Iserrors.As 检测。

errors.Is

检查错误链中是否包含特定错误:

import "errors"

err := readConfig("config.json")
if err != nil {
if errors.Is(err, os.ErrNotExist) {
fmt.Println("配置文件不存在,使用默认配置")
} else if errors.Is(err, os.ErrPermission) {
fmt.Println("没有权限读取配置文件")
} else {
fmt.Println("读取配置文件失败:", err)
}
}

解释errors.Is 会遍历错误链,检查是否包含指定的错误值。

errors.As

从错误链中提取特定类型的错误:

import "errors"

err := getUser(-1)
if err != nil {
var validationErr *ValidationError
if errors.As(err, &validationErr) {
fmt.Printf("验证失败: 字段 %s - %s\n", validationErr.Field, validationErr.Message)
return
}

var notFoundErr *NotFoundError
if errors.As(err, &notFoundErr) {
fmt.Printf("资源未找到: %s (ID: %d)\n", notFoundErr.Resource, notFoundErr.ID)
return
}

fmt.Println("其他错误:", err)
}

解释errors.As 会遍历错误链,尝试将错误转换为指定类型。

Unwrap

手动解包错误:

err := readConfig("config.json")
for err != nil {
fmt.Println("错误:", err)
err = errors.Unwrap(err)
}

自定义错误类型最佳实践

实现 Unwrap 方法

type QueryError struct {
Query string
Err error
}

func (e *QueryError) Error() string {
return fmt.Sprintf("查询错误: %s: %v", e.Query, e.Err)
}

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

解释:实现 Unwrap() 方法使得错误可以被 errors.Iserrors.As 正确处理。

实现多个接口

type TimeoutError struct {
Op string
Err error
}

func (e *TimeoutError) Error() string {
return fmt.Sprintf("%s 超时: %v", e.Op, e.Err)
}

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

func (e *TimeoutError) Timeout() bool {
return true
}

func (e *TimeoutError) Temporary() bool {
return true
}

解释:实现 Timeout()Temporary() 方法可以与 net.Error 接口配合使用。

Panic 和 Recover

Panic 和 Recover 是 Go 的异常机制,但应该谨慎使用。

Panic

Panic 会立即停止当前函数的执行,并开始展开调用栈:

func mustPositive(n int) {
if n < 0 {
panic(fmt.Sprintf("负数不被允许: %d", n))
}
}

适用场景

  • 程序遇到无法恢复的错误
  • 初始化失败
  • 不可能发生的情况(防御性编程)

Recover

Recover 用于捕获 panic,只能在 defer 函数中使用:

func safeOperation() (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("panic 恢复: %v", r)
}
}()

mightPanic()
return nil
}

HTTP 服务器示例

func handler(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
log.Printf("处理请求时发生 panic: %v", err)
http.Error(w, "内部服务器错误", http.StatusInternalServerError)
}
}()

processRequest(r)
}

何时使用 Panic

应该使用 Panic

  • 程序初始化失败
  • 达到不可能的代码路径
  • 关键资源不可用

不应该使用 Panic

  • 文件不存在
  • 网络连接失败
  • 用户输入错误
  • 配置错误

错误处理最佳实践

1. 提供足够的上下文

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

2. 使用哨兵错误

var (
ErrNotFound = errors.New("资源未找到")
ErrUnauthorized = errors.New("未授权")
ErrInvalidInput = errors.New("无效输入")
)

func getItem(id int) (*Item, error) {
if id <= 0 {
return nil, ErrInvalidInput
}

item := findItem(id)
if item == nil {
return nil, ErrNotFound
}

return item, nil
}

func main() {
item, err := getItem(-1)
if errors.Is(err, ErrInvalidInput) {
fmt.Println("请提供有效的ID")
}
}

3. 错误日志记录

func processOrder(orderID int) error {
order, err := getOrder(orderID)
if err != nil {
log.Printf("获取订单失败 [orderID=%d]: %v", orderID, err)
return fmt.Errorf("处理订单失败: %w", err)
}

if err := validateOrder(order); err != nil {
log.Printf("验证订单失败 [orderID=%d]: %v", orderID, err)
return fmt.Errorf("订单验证失败: %w", err)
}

return nil
}

4. 分层错误处理

type ServiceError struct {
Code string
Message string
Err error
}

func (e *ServiceError) Error() string {
if e.Err != nil {
return fmt.Sprintf("[%s] %s: %v", e.Code, e.Message, e.Err)
}
return fmt.Sprintf("[%s] %s", e.Code, e.Message)
}

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

var (
ErrUserNotFound = &ServiceError{Code: "USER_001", Message: "用户不存在"}
ErrInvalidEmail = &ServiceError{Code: "USER_002", Message: "无效的邮箱地址"}
)

5. 使用 errors 包的函数

func main() {
err := doSomething()

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

var pathErr *os.PathError
if errors.As(err, &pathErr) {
fmt.Printf("路径错误: %s\n", pathErr.Path)
}
}

实战示例:数据库操作

package main

import (
"database/sql"
"errors"
"fmt"
"log"
)

var (
ErrUserNotFound = errors.New("用户不存在")
ErrDuplicateEmail = errors.New("邮箱已被使用")
)

type User struct {
ID int
Name string
Email string
}

type UserRepository struct {
db *sql.DB
}

func (r *UserRepository) FindByID(id int) (*User, error) {
query := "SELECT id, name, email FROM users WHERE id = ?"

var user User
err := r.db.QueryRow(query, id).Scan(&user.ID, &user.Name, &user.Email)

if errors.Is(err, sql.ErrNoRows) {
return nil, fmt.Errorf("查找用户失败 [id=%d]: %w", id, ErrUserNotFound)
}
if err != nil {
return nil, fmt.Errorf("查询数据库失败: %w", err)
}

return &user, nil
}

func (r *UserRepository) Create(user *User) error {
query := "INSERT INTO users (name, email) VALUES (?, ?)"

result, err := r.db.Exec(query, user.Name, user.Email)
if err != nil {
return fmt.Errorf("创建用户失败: %w", err)
}

id, err := result.LastInsertId()
if err != nil {
return fmt.Errorf("获取用户ID失败: %w", err)
}

user.ID = int(id)
return nil
}

type UserService struct {
repo *UserRepository
}

func (s *UserService) GetUser(id int) (*User, error) {
user, err := s.repo.FindByID(id)
if err != nil {
return nil, fmt.Errorf("获取用户失败: %w", err)
}
return user, nil
}

func main() {
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
log.Fatal(err)
}
defer db.Close()

repo := &UserRepository{db: db}
service := &UserService{repo: repo}

user, err := service.GetUser(1)
if err != nil {
if errors.Is(err, ErrUserNotFound) {
fmt.Println("用户不存在")
} else {
log.Printf("错误: %v", err)
}
return
}

fmt.Printf("用户: %+v\n", user)
}

小结

  1. 错误是值:Go 将错误作为普通值处理,而不是异常
  2. 显式检查:每个可能出错的操作都应该检查错误
  3. 错误包装:使用 %w 包装错误,保留错误链
  4. errors.Is/As:用于检查和提取错误链中的特定错误
  5. 自定义错误:实现 Error()Unwrap() 方法
  6. Panic/Recover:仅用于真正不可恢复的情况
  7. 提供上下文:错误信息应该包含足够的调试信息
  8. 分层处理:不同层级使用不同的错误类型

练习

  1. 实现一个自定义错误类型,包含错误码、错误消息和原始错误
  2. 编写一个函数,演示错误包装和 errors.Is 的使用
  3. 实现一个 HTTP 客户端,正确处理各种网络错误
  4. 使用 panic/recover 实现一个安全的函数执行器
  5. 编写一个文件处理程序,演示分层错误处理