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代价巨大,毕竟整个程序都会停掉。