错误

教程地址:点击去往视频教程open in new window

1. error

错误处理是开发过程中必不可少的,在golang中使用error接口来表示一个错误,任何实现此接口的类型都能当做一个错误。

// error 接口的定义
type error interface {
	Error() string
}
1
2
3
4

比如以下程序:

type User struct {
	Name string
}
func genUser() *User {
	return nil
}
func main() {
	u := genUser()
	fmt.Println(u.Name)
}
1
2
3
4
5
6
7
8
9
10

上面的程序会出现崩溃,我们知道nil是不能做任何操作的,这时候,我们就应该去定义一个错误,并且判断错误是否为nil,从而判断函数,是否正常运行了

所以,正确的程序应该这样写:

type User struct {
	Name string
}

func genUser() (*User, error) {
	
	return nil, errors.New("user == nil")
}
func main() {
	if u, err := genUser(); err != nil {
		fmt.Println(err)
	} else {
		fmt.Println(u.Name)
	}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

error是一种面向失败的编程,也就是说,每一个函数理论上都应该返回一个error,并且应该去处理此error,虽然这会让你的代码变的难看且麻烦,但同时也会带来程序的稳定和健壮。

使用error有一些经验:

  • error 应该是函数的最后一个返回值。
  • error 不为 nil 时,不应该对其他返回值有所期待。
  • 只需在error最后出现的位置打印错误即可。

2. error的使用

应用程序中出现错误时,使用 errors.New 返回错误

func pay(money float64) error {
	if money < 10 {
		return errors.New("余额不足")
	}
	return nil
}
1
2
3
4
5
6

在有些情况下,我们需要判断错误是否属于某一个特定的错误,怎么办呢?

var MoneyNotEnough = errors.New("余额不足")

func pay(money float64) error {
	if money < 10 {
		return MoneyNotEnough
	}
	return nil
}

func main() {
	err := pay(5)
	if errors.Is(err, MoneyNotEnough) {
		fmt.Println("两个错误一致")
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

但是如果按下面的方式写:

var MoneyNotEnough = errors.New("余额不足")

func pay(money float64) error {
	if money < 10 {
		return errors.New("余额不足")
	}
	return nil
}

func main() {
	err := pay(5)
	if errors.Is(err, MoneyNotEnough) {
		fmt.Println("两个错误一致")
	} else {
        //这里会打印
		fmt.Println("两个错误不一致")
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

这个原因在于errors.New返回的是一个指针,每次New得到的地址都不同,这种设计可以防止如果两个错误是不同的错误,但是其中的字符串相同,errors.Is不会判断其是一种错误(可能面试会问)

除了使用errors.New 返回错误,我们也可以使用fmt.Errorf()

func pay(money float64) error {
	if money < 10 {
		return fmt.Errorf("余额不足:%f", money)
	}
	return nil
}
1
2
3
4
5
6

errors.As()用于将error转换为具体的error类型

type MyError struct {
	Msg string
}

func (e *MyError) Error() string {
	return e.Msg
}

var MoneyNotEnough = &MyError{Msg: "余额不足"}

func pay(money float64) error {
	if money < 10 {
		return MoneyNotEnough
	}
	return nil
}

func main() {
	err := pay(5)
	var myError *MyError
	if errors.As(err, &myError) {
		fmt.Println("转换成功")
		fmt.Println(myError.Error())
	} else {
		fmt.Println("不是此error类型")
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

使用fmt.Errorf(),还可以将一个错误进行封装,封装后的错误和之前的相等

func main() {
	originalErr := errors.New("原始错误")
    //%w 嵌套生成一个新的错误
	newError := fmt.Errorf("error: %w", originalErr)
	if errors.Is(newError, originalErr) {
		fmt.Println("这两个错误相等")
	}
	//用于将一个错误对象展开,得到下一层错误对象
	originalErr1 := errors.Unwrap(newError)
	if errors.Is(originalErr, originalErr1) {
		fmt.Println("这两个错误相等")
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13

还可以是属于errors.Join进行多个错误的封装,这是go1.20版本的新特性

func main() {
	err1 := errors.New("err1")
	err2 := errors.New("err2")
	err := errors.Join(err1, err2)
	if errors.Is(err, err1) {
		fmt.Println("err is err1")
	}
	if errors.Is(err, err2) {
		fmt.Println("err is err2")
	}
}
1
2
3
4
5
6
7
8
9
10
11

3. panic

panic也是一种错误,只是panic会导致程序直接退出。一般只有遇到不可恢复的错误才使用panic

func main() {
	err := pay(5)
	if err != nil {
        //panic会打印详细的堆栈信息,便于定位错误
		panic("余额不足")
	}
}
1
2
3
4
5
6
7

程序往往会有预料不到的错误发生,会导致panic,导致程序崩溃,这是不可接受的,所以我们可以捕获panic

func main() {
	err := pay(5)
    //defer 延迟调用,在return结束前运行
	defer func() {
        //panic会被捕获
		if err := recover(); err != nil {
			log.Printf("panic:%+v \n", err)
		}
	}()
	if err != nil {
		panic("余额不足")
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
  • 利用recover处理panic指令,defer 必须放在 panic 之前定义
  • recover 只有在 defer 调用的函数中才有效

如果发生了panic,我们还想要程序继续执行可以这样做

func main() {
    //使用匿名函数 将需要保护的代码 控制在一定范围
	func() {
		err := pay(5)
		defer func() {
			if err := recover(); err != nil {
				log.Printf("panic:%+v \n", err)
			}
		}()
		if err != nil {
			panic("余额不足")
		}
	}()
	fmt.Println("发生panic后 这里的代码依旧会执行")
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15