Go中对错误的判断与处理

107 阅读3分钟

overview

Go中的错误本质是一个interface变量

type error interface {
	Error() string
}

对于一个错误,我们的处理逻辑是这样的:先判断,比如它是否是一个我们已知的错误,再处理,比如打印错误信息等。

本文将对这两部分进行探讨。

判断

Go对错误的判断有两种:errors.Is, errors.As

以下我们分别来看一下两个函数的形式,作用和用法

errors.Is

func Is(err, target error) bool{
	//
}

errors.Is接受两个error类型的参数,它将判断target是否匹配err tree上的某节点;

什么是error tree?首先要理解,错误是可以不断包装的,比如下面这样:

var (
    ErrDatabaseNotFound = errors.New("database not found")
    ErrUserNotFound     = errors.New("user not found")
)

func QueryUserFromDatabase(userID int) (User, error) {
    // 假设数据库查询失败
    return User{}, fmt.Errorf("query user with ID %d: %w", userID, ErrDatabaseNotFound)
}

func GetUserProfile(userID int) (Profile, error) {
    user, err := QueryUserFromDatabase(userID)
    if err != nil {
        return Profile{}, fmt.Errorf("get profile for user with ID %d: %w", userID, err)
    }
    // 返回用户的Profile
    return Profile{UserID: user.ID, Name: user.Name}, nil
}

这里我们利用fmt.Errorf,实现了对原始error的不断包装,而这一层层的包装就形成了error tree;

当我们使用errors.Is时,函数内部会调用unwrap方法,不断的对error tree进行解封装,直到遇到的error就是我们传入的target,假如解到最后也不符合target,那就会返回false;

比如:

func main() {
    _, err := GetUserProfile(123)
    if err != nil {
        if errors.Is(err, ErrDatabaseNotFound) {
            fmt.Println("Database error:", err)
        } else if errors.Is(err, ErrUserNotFound) {
            fmt.Println("User error:", err)
        } else {
            fmt.Println("Unknown error:", err)
        }
    }
}

errors.As

func As(err error, target any) bool {
//
}

errors.As接受一个error类型的err和any类型的target,它的作用就是判断err tree上是否存在与target同一类型的error

比如:

type DatabaseError struct {
    Code    int
    Message string
}

func (e *DatabaseError) Error() string {
    return fmt.Sprintf("DB Error (Code: %d): %s", e.Code, e.Message)
}

func QueryDatabase() error {
    // 假设数据库出现某种问题
    return &DatabaseError{
        Code:    404,
        Message: "Record not found",
    }
}

func main() {
    err := QueryDatabase()
    if err != nil {
        var dbErr *DatabaseError
        if errors.As(err, &dbErr) {
            fmt.Printf("Database error occurred with code %d and message: %s\n", dbErr.Code, dbErr.Message)
        } else {
            fmt.Println("An unknown error occurred:", err)
        }
    }
}

以上例子中,我们自定义了一个错误类型DatabaseError,当err tree上有节点符合这个类型时,它返回true,否则返回false;

使用场景

当我们需要对一个确切的错误与error tree进行匹配时,我们使用errors.Is

当我们需要进行的是类型的匹配时(尤其是自定义类型),我们使用errors.As

处理

常规的error处理一般形式就是打日志,返回错误信息,但是当遇到一些关键性的错误时,往往要用panic及recover机制;

panic

panic("ERROR!")

panic语句使得当前函数立即终止,同时它还具有传播性,比如函数A调用了函数B,函数B遇到panic,会不断传播,使得A也停止,直到main函数panic掉,程序退出;

recover

if r :=recover();r {
//TODO S
}

前面说过,panic会不断传播,而reover会终止这个传播,夺回程序控制权,比如之前那个AB函数的例子,假如A中有一个recover函数,那么panic传播到A时,便被recover截获,不会再传播,而此时程序会运行上面代码中s处的处理逻辑。

值得注意的是,panic与recover只应该用于严重错误的处理,常规错误使用panic代价巨大,毕竟整个程序都会停掉。