Menu
CoddyTech

errors.Is() 和 errors.As()

本课属于 Coddy GO 学习旅程的“面向对象编程”单元。第 56 课,共 107 课。

错误被包装后,像 err == ErrNotFound 这样的简单相等性检查就不再有效。errors 包提供了两个函数,用于检查包装后的错误链:errors.Is() 和 errors.As()。

errors.Is() 会检查错误链中的任何错误是否与特定的哨兵错误匹配:

import "errors"

var ErrNotFound = errors.New("not found")

func FindUser(id int) error {
    return fmt.Errorf("database lookup failed: %w", ErrNotFound)
}

err := FindUser(42)
if errors.Is(err, ErrNotFound) {
    fmt.Println("User doesn't exist")
}

即使返回的错误被包装起来,errors.Is() 也会解包错误链,并找到其中的 ErrNotFound。

errors.As() 会检查错误链中的任何错误是否与特定的类型匹配,并提取该错误:

type ValidationError struct {
    Field string
}

func (e *ValidationError) Error() string {
    return "validation failed: " + e.Field
}

err := fmt.Errorf("request failed: %w", &ValidationError{Field: "email"})

var ve *ValidationError
if errors.As(err, &ve) {
    fmt.Println("Invalid field:", ve.Field)
}

检查哨兵错误时使用 errors.Is(),需要访问自定义错误类型的字段时使用 errors.As()。这两个函数都会遍历整个错误链,因此对于正确处理包装后的错误至关重要。

challenge icon

挑战

简单

让我们构建一个文件处理系统,演示如何使用 errors.Is() 和 errors.As() 正确检查包装后的错误链。你将创建错误,让它们在经过不同层时被包装,然后使用这些函数识别错误并提取其中的信息。

你将把代码组织在两个文件中:

  • errors.go:为文件系统定义错误基础结构。

    创建一个哨兵错误:

    • ErrPermissionDenied,错误消息为 "permission denied"

    创建一个自定义错误类型:

    • FileError 结构体,包含 Filename(字符串)和 Operation(字符串)字段
    • 它的 Error() 方法应返回:[Operation] failed on file: [Filename]

    实现两个模拟文件操作并包装错误的函数:

    • ReadFile(filename string) error:如果 filename 是 "secret.txt",则返回 ErrPermissionDenied。如果 filename 是 "missing.txt",则返回一个 *FileError,其中 Operation 为 "read",Filename 为给定的文件名。否则返回 nil。
    • ProcessFile(filename string) error:调用 ReadFile。如果返回错误,则使用 fmt.Errorf 和格式 "processing failed: %w" 包装该错误。否则返回 nil。
  • main.go:从输入中读取文件名并调用 ProcessFile。使用 errors.Is() 和 errors.As() 检查包装后的错误链,并根据找到的内容打印相应消息。

输入内容如下:

  • 第 1 行:要处理的文件名

通过检查错误链来处理结果:

  • 如果 errors.Is() 在错误链中找到 ErrPermissionDenied:打印完整的错误消息,然后换行打印 Access denied - check file permissions
  • 如果 errors.As() 在错误链中找到 *FileError:打印完整的错误消息,然后换行打印 File issue: [Filename] during [Operation]
  • 如果没有发生错误:打印 File '[filename]' processed successfully

例如,输入 secret.txt 时,输出应为:

processing failed: permission denied
Access denied - check file permissions

输入 missing.txt 时,输出应为:

processing failed: read failed on file: missing.txt
File issue: missing.txt during read

输入 data.txt 时,输出应为:

File 'data.txt' processed successfully

请注意,尽管错误被 ProcessFile 包装,你仍然可以使用 errors.Is() 检测原始哨兵错误,并使用 errors.As() 提取自定义错误类型。这些函数会遍历整个错误链,查找你要找的内容。

亲手试试

package main

import (
	"errors"
	"fmt"
)

func main() {
	// 从输入中读取文件名
	var filename string
	fmt.Scanln(&filename)

	// 用文件名调用 ProcessFile
	err := ProcessFile(filename)

	// TODO:检查错误链并处理结果
	// 
	// 用 errors.Is() 检查错误链中是否包含 ErrPermissionDenied:
	// 如果找到了:先打印完整的错误,再打印 "Access denied - check file permissions"
	//
	// 用 errors.As() 检查错误链中是否包含 *FileError:
	// 如果找到了:先打印完整的错误,再打印 "File issue: [Filename] during [Operation]"
	//
	// 如果没有错误:打印 "File '[filename]' processed successfully"

	_ = errors.Is // 提示:使用 errors.Is()
	_ = errors.As // 提示:使用 errors.As()
	_ = err
}
quiz icon自我测试

这节课包含一个小测验。开始学习即可作答,并记录你的进度。

面向对象编程中的所有课

自己练习: Go 在线编译器