📖 学习课程:

📅 2024年5月12日

📦 使用版本为 1.21.5

错误处理和测试

⭐️ Go 没有像 Java 和 .NET 那样的 try/catch 异常机制:不能执行抛异常操作。但是有一套 defer-panic-and-recover 机制

准确的来说,Go并没有异常,更多的是通过错误来体现,同样的,Go中也并没有 try-catch-finally这种语句,Go创始人希望能够将错误可控,以编程的方式来控制错误,他们不希望干什么事情都需要嵌套一堆 try-catch,所以大多数情况会将其作为函数的返回值来返回

⭐️ 通过在函数和方法中返回错误对象作为它们的唯一或最后一个返回值——如果返回 nil,则没有错误发生——并且主调(calling)函数总是应该检查收到的错误,通过这个方法来获取到错误并处理

⭐️ 库函数通常必须返回某种错误提示给主调(calling)函数

⭐️ 在 Go中错误的级别有三种

  • error:部分流程出错,需要处理
  • panic:很严重的问题,程序应该在处理完问题后立即退出
  • fatal:非常致命的问题,程序应该立即退出

🌟 Error

⭐️ Error属于正常的流程错误,它并不足以影响程序停止

⭐️ erro本身是一个接口,并且只有一个方法

//源代码
type error interface {
	Error() string
}

1️⃣ 创建错误

⭐️ 通过 errors包下的 New函数来创建或者通过 fmt.Errorf创建接口

下面是 New函数源码

//New源码
func New(text string) error {
	return &errorString{text} //直接返回一个errorString对象
}

//errorString对象源码
type errorString struct {
	s string
}

创建错误对象

func main() {
	err1 := errors.New("错误1") //错误1是提示语句
	err2 := fmt.Errorf("错误2") //和 fmt.Printf() 完全一样,接收有一个或多个格式占位符的格式化字符串和相应数量的占位变量。和打印信息不同的是它用信息生成错误对象
}

列子:

func main() {
	err1 := errors.New("错误1")
	//err2 := fmt.Errorf("错误2")
	sumNum, err := sum(-1, 2, err1)
	if err != nil {
		fmt.Printf("%v", err)
	} else {
		fmt.Printf("%v", sumNum)
	}

}

func sum(a, b int, err error) (int, error) { //返回了两个值,一个是整数,一个是错误
	if a < 0 || b < 0 { // 如果小于0就返回错误
		return -1, err // 返回错误
	} else {
		return a + b, nil //如果值正确,就正常返回,值默认为nil
	}
}

⭐️ 大部分情况,为了更好的维护性,一般都不会临时创建 error,而是会将常用的 error当作全局变量使用

2️⃣ 自定义错误

⭐️ 可以通过实现 Error()方法来实现自定义 error,因为默认底层 errorString实现太过于简单,表达能力不足,所以很多开源库包括官方库都会选择自定义 error,以满足不同的错误需求


type customError struct {
	s     string //报错提示
	code  int    //错误代码
	class string //错误类型
}

func (c *customError) Error() string { //实现Error接口
	return c.s
}

func (c *customError) toString() string { //自定义toString方法
	return fmt.Sprintf("{code: %d , class: %s , s: %s}", c.code, c.class, c.s)
}

func main() {
	err := &customError{ //创建自定义错误类的对象
		s:     "error",
		code:  -2,
		class: "io",
	}
	fmt.Printf("%v", err.toString())
}

3️⃣ 传递

⭐️ 像第一个创建错误中的列子一样,调用 sum函数的调用者函数,如果不是 main函数,是一个其他函数,那么它有两种方法去处理这个函数一个是自己处理这个函数,还有一个办法则是将错误作为放回置抛给上一层函数,这种方法就叫做传递

但是这样会出现一个问题,错误在传递的过程中可能会层层包装,当上层调用者想要判断错误的类型来做出不同的处理时,可能会无法判别错误的类别或者误判,而链式错误正是为了解决这种情况而出现的

⭐️ 在 fmt 包中提供了新的 wrapError 类型,来使用链式错误,

wrapError 实例需要通过 fmt.Errorf() 格式化的方式创建因为 wrapError它并不对外暴露,Go 1.13 新增加了 %w 格式化动词用于匹配 error 对象。如果 fmt.Errorf() 存在 %w,则内部创建 wrapError 对象,并把 %w 匹配到的 error 对象存储到 wrapError.err 属性中,最终返回 wrapError 对象。如果没有 %w,则 fmt.Errorf() 内部还是走原始的 errors.New() 的方式返回 errorString 对象

//源代码
type wrapError struct {
    msg string //错误信息
    err error
}

func (e *wrapError) Error() string {
    return e.msg
}

func (e *wrapError) Unwrap() error {
    return e.err
}

⭐️ 如果这样一级一级的创建和调用,就像链表一样,就叫做链式错误

列子:

func main() {
	err := func1()
	if err != nil {
		fmt.Println("Error:", err)
	}
}

func func1() error {
	err := func2()
	if err != nil {
		return fmt.Errorf("上级报错为: %w", err)
	}
	return nil
}

func func2() error {
	_, err := os.Open("non_existent_file.txt")
	if err != nil {
		return fmt.Errorf("原始报错为: %w", err) //闯入原始报错,创建wrapError对象
	}
	return nil
}

//输出:
Error: 上级报错为: 原始报错为: open non_existent_file.txt: The system cannot find the file specified.

4️⃣ 处理错误

⭐️ errors包提供了几个方便函数用于处理错误

⭕️ Unwrap()函数用于解包一个错误链(注意这里是 errors包的),但是解包后可能解包的错误上面还套了一层错误

//源代码
func Unwrap(err error) error {
   u, ok := err.(interface { // 类型断言,是否实现该方法
      Unwrap() error
   })
   if !ok { //没有实现说明是一个基础的error
      return nil
   }
   return u.Unwrap() // 否则调用Unwrap
}

解包的例子:


func main() {
	err := func1()
	err2 := errors.Unwrap(err) //解包,也就是将func1()放回的错误解开了,打开一看里面还有一个报错那就是fun2()的
	fmt.Printf("%v", err2) 
}

func func1() error {
	err := func2()
	if err != nil {
		return fmt.Errorf("上级报错为: %w", err)
	}
	return nil
}

func func2() error {
	_, err := os.Open("non_existent_file.txt")
	if err != nil {
		return fmt.Errorf("原始报错为: %w", err)
	}
	return nil
}

//输出
原始报错为: open non_existent_file.txt: The system cannot find the file specified.

⭕️ 如果想要在错误链中找到对应的值或类型,可以递归进行查找匹配,可以使用 Is函数,判断错误链中是否包含指定的错误

//源代码
func Is(err, target error) bool {
	if target == nil {
		return err == target
	}

	isComparable := reflectlite.TypeOf(target).Comparable()
	for {
		if isComparable && err == target {
			return true
		}
		if x, ok := err.(interface{ Is(error) bool }); ok && x.Is(target) {
			return true
		}
		switch x := err.(type) {
		case interface{ Unwrap() error }:
			err = x.Unwrap()
			if err == nil {
				return false
			}
		case interface{ Unwrap() []error }:
			for _, err := range x.Unwrap() {
				if Is(err, target) {
					return true
				}
			}
			return false
		default:
			return false
		}
	}
}

Is函数列子:

// 定义原始报错,以便Is判断
var (
	oneErro = fmt.Errorf("这是原始报错为")
)

func main() {
	err := func1()
	if errors.Is(err, oneErro) { // 判断是否包含这个错误
		fmt.Println("oneErro")
	}
}

func func1() error {
	err := func2()
	if err != nil {
		twoErr := fmt.Errorf("上级报错为: %w", err)
		return twoErr
	}
	return nil
}

func func2() error {
	_, err := os.Open("non_existent_file.txt")
	if err != nil {
		return oneErro
	}
	return nil
}

⭕️ errors.As函数的作用是在错误链中寻找第一个类型匹配的错误,并将值赋值给传入的 err ,些情况下需要将 error类型的错误转换为具体的错误实现类型,以获得更详细的错误细节,而对一个错误链使用类型断言是无效的,因为原始错误是被结构体包裹起来的,这也是为什么需要 As函数的原因

As函数接受一个类型参数,并尝试将 error接口的值转换为这个类型。如果转换成功,As会返回 true并将转换后的值赋给传入的变量;如果转换失败,则返回 false并保持变量不变

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

target必须是指向 error的指针,由于在创建结构体时返回的是结构体指针,所以 error实际上 *TimeError类型的,那么 target就必须是 **TimeError类型的

❓ 反正我初略的理解就是测试这个错误是否包含这个自定义错误,下面是一个列子


type TimeError struct { // 自定义error
	Msg  string
	Time time.Time //记录发生错误的时间
}

func (m TimeError) Error() string {
	return m.Msg
}

func NewMyError(msg string) error {
	return &TimeError{
		Msg:  msg,
		Time: time.Now(),
	}
}

func wrap1() error { // 包裹原始错误
	return fmt.Errorf("wrapp error %w", wrap2())
}

func wrap2() error { // 原始错误
	return NewMyError("original error")
}

func main() {
	var myerr *TimeError
	err := wrap1()
	// 检查错误链中是否有*TimeError类型的错误
	if errors.As(err, &myerr) { // 输出TimeError的时间
		fmt.Println("original", myerr.Time)
	}
}

⭐️ 官方提供的 errors包其实并不够用,因为它没有堆栈信息,不能定位,一般会比较推荐使用官方的另一个包

🌟Panic

⭐️ Panic表示就是十分严重程序问题,程序需要立即停止处理改问题,否者就停止运行并输出堆栈信息,不过 panic在退出之前会做好程序的善后工作,同时 panic也可以被恢复来保证程序继续运行

⭐️ 只要任意协程发生 panic,如果不及时处理,整个程序都会奔溃

1️⃣ 创建 painc

⭐️ 可以直接使用 panic函数创建

//源代码
func panic(v any)

列子:

func main() {
	initDataBase("", 0)
}

func initDataBase(host string, port int) {
	if len(host) == 0 || port == 0 {
		panic("非法的数据链接参数")
	}
}

//输出:
panic: 非法的数据链接参数

goroutine 1 [running]:
main.initDataBase(...)
        D:/learnCode/Go/gosql/gosql.go:9
main.main()
        D:/learnCode/Go/gosql/gosql.go:4 +0x25
exit status 2

2️⃣ 善后

⭐️ 程序因为 panic退出之前会做一些善后工作,例如执行 defer语句(前面说过了)

⭐️ 在 defer中也可以嵌套 panic,执行顺序也是一样的,发生 panic时后续的逻辑将无法执行

⭐️ 当发生 panic时,会立即退出所在函数,并且执行当前函数的善后工作,例如 defer,然后层层上抛,上游函数同样的也进行善后工作,直到程序停止运行

func main() {
	defer fmt.Println("A")
	defer func() {
		func() {
			panic("panicA")
			defer fmt.Println("E")
		}()
	}()
	fmt.Println("C")
	dangerOp()
	defer fmt.Println("D")
}

func dangerOp() {
	defer fmt.Println(1)
	defer fmt.Println(2)
	panic("panicB")
	defer fmt.Println(3)
}


//输出:
C
2                                                       
1                                                       
A                                                       
panic: panicB                                           
        panic: panicA  

⭐️ 当子协程发生 panic时,不会出发当前写出的善后工作,如果直到子协程退出都没有恢复 panic,那么程序将会直接停止运行(协程后面会学)

var waitGroup sync.WaitGroup

func main() {
	demo()
}

func demo() {
	waitGroup.Add(1)
	defer func() {
		fmt.Println("A")
	}()
	fmt.Println("C")
	go dangerOp() //创建协程
	waitGroup.Wait() // 父协程阻塞等待子协程执行完毕
	defer fmt.Println("D")
}
func dangerOp() {
	defer fmt.Println(1)
	defer fmt.Println(2)
	panic("panicB")
	defer fmt.Println(3)
	waitGroup.Done()
}

3️⃣ 恢复

⭐️ 当发生 panic时,使用内置函数 recover()可以及时的处理并且保证程序继续运行,必须要在 defer语句中运行,使用示例如下

func main() {
	dangerOp()
	fmt.Println("程序正常退出")
}

func dangerOp() {
	defer func() {
		if err := recover(); err != nil {
			fmt.Println(err)
			fmt.Println("panic恢复")
		}
	}()
	panic("发生panic")
}

//输出:
发生panic
panic恢复
程序正常退出

但事实上 recover()的使用有许多隐含的陷阱。例如在 defer中再次闭包使用 recover

func main() {
	dangerOp()
	fmt.Println("程序正常退出")
}

func dangerOp() {
	defer func() {
		func() {
			if err := recover(); err != nil {
				fmt.Println(err)
				fmt.Println("panic恢复")
			}
		}()
	}()
	panic("发生panic")
}

⭐️闭包函数可以看作调用了一个函数,panic是向上传递而不是向下,自然闭包函数也就无法恢复 panic,所以输出如下。

panic: 发生panic  

⭐️ 除此之外,还有一种很极端的情况,那就是 panic()的参数是 nil

func main() {
   dangerOp()
   fmt.Println("程序正常退出")
}

func dangerOp() {
   defer func() {
      if err := recover(); err != nil {
         fmt.Println(err)
         fmt.Println("panic恢复")
      }
   }()
   panic(nil)
}

这种情况 panic确实会恢复,但是不会输出任何的错误信息。

输出

程序正常退出

总的来说 recover函数有几个注意点

  1. 必须在 defer中使用
  2. 多次使用也只会有一个能恢复 panic
  3. 闭包 recover不会恢复外部函数的任何 panic
  4. panic的参数禁止使用 nil

🌟fatal

fatal是一种极其严重的问题,当发生 fatal时,程序需要立刻停止运行,不会执行任何善后工作,通常情况下是调用 os包下的 Exit函数退出程序,如下所示

func main() {
	dangerOp("")
}

func dangerOp(str string) {
	if len(str) == 0 {
		fmt.Println("fatal")
		os.Exit(1)
	}
	fmt.Println("正常逻辑")
}

输出

fatal

fatal级别的问题一般很少会显式的去触发,大多数情况都是被动触发。