这是我参与「第三届青训营 -后端场」笔记创作活动的第1篇笔记。
最近几节课下来,跟着老师们写了几个小项目,虽然熟悉了Go的语法和一些常见标准包的使用,项目的大体流程也能够理解,但是由于课程还是比较硬核,一些细节还是没有掌握,比如error的使用,虽然能够照猫画虎地用起来,但是要优雅地用还是得认真梳理一下。
Golang中error的个人理解
1. 使用场景
Golang不像Java使用throw->(try, catch, finally)来处理Checked Exception,在Go看来,把能够预见的“异常”都视作普通错误,开发者可以通过特定的代码解决这些可预见的错误,这些错误通常不会引起程序的崩溃。而数组越界、除数为0这种会引起程序崩溃的错误在Go中则被认为是真正的异常,需要使用panic和recovery来处理。因此,在Go中使用多值返回来进行普通错误的捕获和处理。例如:打开一个文件时可能出现“文件不存在”的情况,我们就可以通过返回的error来进行流程控制。通过这种方式,一定程度上实现了代码的简洁优雅,不会像Java等语言一样出现Exception的滥用。
// 在某些情况下我们需要通过文件来进行一些工作
open, err := os.Open("test.txt")
// 但是如果出现一些错误就无法进行后续操作
if err != nil {
// 错误处理
}
2. error的结构
首先,需要明白error是一个类型(接口)!
error和int, byte, bool等数据类型一样,定义在内置库builtin中,它是表示错误的常规接口,nil代表没有错误。因此,我们可以实现这个接口来实现自定义的error。
// 官方文档中对error的定义
// 内置的error接口类型是表示错误的常规接口,nil代表无错误
type error interface {
Error() string
}
我们也可以通过这个接口自定义error:
type MyError struct {
ErrorTime time.Time
Msg string
}
// 实现error接口中的Error方法
func (myError *MyError) Error() string {
return myError.Msg
}
3. errors标准库(包)
需要与error区别开,errors是一个标准库(包)!
errors包中实现了4个用于操作error的函数,包括
-
errors.New(s string)error: 创建一个错误。 -
errors.Unwrap(err error)error: 从错误链中拆解错误。 -
errors.Is(err, target error)bool: 判断错误链中是否存在与target匹配的错误。特别的,如果该错误与target相等,或者它实现了一个方法is(error)bool,使is(target)返回true,则认为该错误与target匹配。 -
errors.As(err error, target any)bool: 查找错误链中与target匹配的第一个错误,如果存在则将该错误值赋值给target并返回true,否则返回false。特别的,如果错误的具体值可以分配给target所指向的变量,或者如果该错误有一个方法As(interface{})bool,使得As(target)返回true,那么该错误就与target匹配。
需要注意的是,拆解错误的函数在errors包中而嵌套错误则需要使用fmt.Errorf(“%w”, err) error,其中w可以理解为wrap来方便记忆使用。
4. error的基本使用
- 创建
errors.New(s string)error
// 使用举例 err := errors.New("This is an error!")fmt.Errorf(s string)error
// 使用举例 err := fmt.Errorf("This is an error!") - 嵌套(将一个
error关联到一条错误链中)fmt.Errorf("%w", err)error
// 使用举例 err1 := errors.New("error-1") err2 := fmt.Errorf("error-2 [%w]", err1) // 打印一下两个err看看 fmt.Printf("The type of err1: %T\n", err1) fmt.Printf("The content of err1: %v\n", err1) fmt.Printf("The type of err2: %T\n", err2) fmt.Printf("The content of err2: %v\n", err2) // output: // The type of err1: *errors.errorString // The content of err1: error-1 // The type of err2: *fmt.wrapError // The content of err2: error-2 [error-1] - 拆解(将一个
error从错误链中拆解下来)errors.Unwrap(err error)error
// 使用举例 err1 := errors.New("error-1") err2 := fmt.Errorf("error-2 [%w]", err1) fmt.Println(err2) // 再拆解 err := errors.Unwrap(err2) fmt.Printf("Unwrap once: %v\n", err) err = errors.Unwrap(err) fmt.Printf("Unwrap twice: %v\n", err) // output: // error-2 [error-1] // Unwrap once: error-1 // Unwrap twice: <nil> // 通过第二次拆解可以看出: // 对单个error拆解后返回nil - 判断是否存在匹配
errors.Is(err, target error)bool
// 使用举例 err1 := errors.New("error-1") err2 := fmt.Errorf("error-2 [%w]", err1) err3 := fmt.Errorf("error-3 [%w]", err2) fmt.Println(err3) // 判断错误链err3中是否存在与err1匹配的错误 fmt.Printf("err1 in err3: %v\n", errors.Is(err3, err1)) // output: // error-3 [error-2 [error-1]] // err1 in err3: true - 查找第一个匹配并赋值
errors.As(err error, target any)bool
// 定义一个自定义的错误 type MyError struct { ErrorTime time.Time Msg string Err error } // 实现error接口中的Error()方法 func (myErr MyError) Error() string { timeStr := myErr.ErrorTime.Format("2006-01-02 15:04:05") return timeStr + " " + myErr.Msg }// 使用举例 err1 := errors.New("error-1") err2 := fmt.Errorf("error-2 [%w]", err1) err3 := fmt.Errorf("error-3 [%w]", err2) var t1 any // any和一切类型匹配,所以第一个输出应该为true fmt.Println(errors.As(err3, &t1), t1) var t2 error // error和error匹配,所以第一个输出也为true fmt.Println(errors.As(err3, &t2), t2) var t3 MyError // MyError和error无法匹配,所以第一个输出为false fmt.Println(errors.As(err3, &t3), t3) // 定义一个MyError类型实例 myError := MyError{ ErrorTime: time.Now(), Msg: "DIY error", } // MyError和MyError匹配,所以第一个输出为true fmt.Println(errors.As(myError, &t3), t3) // output: // true error-3 [error-2 [error-1]] // true error-3 [error-2 [error-1]] // false 0001-01-01 00:00:00 // true 2022-05-12 02:29:16 DIY error