错误比较

在 Go 语言中,对 error 类型的错误比较有多种形式,其中一些看似简单但隐藏着陷阱。以下是错误比较的详细分析和常见陷阱:

🎯 错误比较的 4 种形式

1. 常规错误比较 (err == target)

if err == io.EOF {
    // 处理 EOF 情况
}

​适用场景​​:比较预定义的哨兵错误(全局变量)

2. 与 nil 比较 (err == nil)

if err != nil {
    // 错误处理
}

​安全场景​​:检查函数是否返回错误

3. errors.Is()(推荐)

if errors.Is(err, io.EOF) {
    // 检查错误链中是否存在特定错误
}

​适用场景​​:处理包装错误(Wrapped Errors)

4. errors.As() 类型匹配

var e *os.PathError
if errors.As(err, &e) {
    // 处理特定错误类型
}

​适用场景​​:处理自定义错误类型


🧩 最危险的错误比较陷阱

❌ 陷阱 1:直接比较动态创建的字符串错误

func ReadConfig() error {
    return fmt.Errorf("config error")
}

// 错误的比较方式
if err == fmt.Errorf("config error") {
    // 永远不会执行(每次返回新实例)
}

​原因​​:fmt.Errorf() 每次返回新实例,内存地址不同

❌ 陷阱 2:比较被包装的错误

wrappedErr := fmt.Errorf("上下文: %w", io.EOF)

// 错误比较
if wrappedErr == io.EOF { 
    // 永远不会执行
}

​正确做法​​:

if errors.Is(wrappedErr, io.EOF) {
    // 正确匹配
}

❌ 陷阱 3:忽略自定义错误的不相等性

type CustomError struct{ Code int }

err1 := CustomError{Code: 404}
err2 := CustomError{Code: 404}

fmt.Println(err1 == err2) // false(编译错误) ❌

// 只有添加比较方法才可编译(但仍不建议)
func (e CustomError) Equal(other CustomError) bool {
    return e.Code == other.Code
}

❌ 陷阱 4:错误与接口变量比较

var target error = io.EOF
err := io.EOF
fmt.Println(err == target) // true ✅

var anyInterface interface{} = io.EOF
fmt.Println(err == anyInterface) // true ✅

// 危险比较
var reader io.Reader = io.EOF
fmt.Println(err == reader) // false ❌

​原因​​:io.EOF 实现了 errorio.Reader,但接口类型不同


✅ 错误处理最佳实践对比

​错误处理方式​

​使用场景​

​示例​

​推荐度​

err == nil

基础错误检查

if err != nil {...}

★★★★★

errors.Is()

匹配错误链

errors.Is(err, io.EOF)

★★★★★

errors.As()

类型断言

errors.As(err, &PathError)

★★★★★

err == target

预定义哨兵错误

err == io.EOF

★★☆☆☆

直接比较字符串

任意场景

err.Error() == "msg"

☆☆☆☆☆


🔧 自定义错误处理模式

1. 实现 Is() 方法(支持错误链)

type DatabaseError struct { Code int }

func (e DatabaseError) Is(target error) bool {
    t, ok := target.(DatabaseError)
    return ok && e.Code == t.Code
}

func main() {
    err := DatabaseError{Code: 404}
    wrapped := fmt.Errorf("包裹错误: %w", err)
    
    // 正确比较
    if errors.Is(wrapped, DatabaseError{Code: 404}) {
        fmt.Println("匹配 404 错误")
    }
}

2. 实现 Equal() 方法(用于直接比较)

func (e DatabaseError) Equal(target DatabaseError) bool {
    return e.Code == target.Code
}

// 比较
err := DatabaseError{Code: 404}
if err.Equal(DatabaseError{Code: 404}) {
    // 精确比较
}

3. 使用常量哨兵错误(最安全)

var ErrNotFound = errors.New("未找到资源")

func main() {
    err := getResource()
    if errors.Is(err, ErrNotFound) {
        // 安全处理
    }
}

🚨 需要警惕的比较结果

比较表达式
结果
原因

fmt.Errorf("A") == fmt.Errorf("A")

false

不同内存实例

io.EOF == io.EOF

true

全局单实例

errors.New("A") == errors.New("A")

false

不同内存实例

error(nil) == nil

true

类型匹配

var e error = nil; e == nil

true

接口值为 nil

var e error = (*Error)(nil); e == nil

false

非空接口类型

​黄金规则​​:对于重要的错误处理,​​总是​​使用 errors.Is()errors.As() 代替 == 操作符

通过遵循这些原则,您可以避免常见的错误比较陷阱,编写出更健壮、更易维护的错误处理代码。

最后更新于