错误与异常
Info
意料之中的叫 错误
意料之外的叫 异常
错误:是指可能出现问题的地方出现了问题,比如打开一个文件时失败,这种情况是在意料之中的。
异常:是指不应该出现问题的地方出现了问题,比如引用了空指针,这种情况在人们的意料之外。
所以错误是业务过程的一部分,而异常不是。
错误
Go 中错误是一种类型。
错误用内置的 error类型表示。和 int、float64 是等价的。
error 是一个接口,接口是一种类型,所以说 error 是一种类型。
| type error interface {
Error() string
}
|
error类型可以存储错误值,从函数中返回等等。
| package main
import (
"fmt"
"os"
)
func main() {
f, err := os.Open("test.txt")
if err != nil {
fmt.Println(err)
return
}
// 根据f进行文件读写
fmt.Println(f.Name(), "opened successfully")
}
|
抛出错误
在编写桩模块供人调用时,我们不知道调用者会传进来什么参数,所以我们需要对传进来的参数做判断。
如果发生错误我们需要进行处理,或者 抛出错误。
要抛出错误,那得先会创建错误。
创建错误有两种方式:
- 通过
errors 包的 New() 函数创建:func New(errMsg string) error
- 通过
fmt 包的 Errorf() 函数创建:func Errorf(format string, a ...interface{}) error
创建错误
| errors.New(string)
fmt.Errorf(string, ...interface{})
|
这两种方式区别在于:
fmt.Errorf() 可以使用格式化字符来返回错误信息,而 errors.New() 不能。
Example
| func div(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("除数小于0")
}
return a / b, nil
}
|
调用的效果:
| fmt.Println(div(10, 0)) // 除数小于0
|
| func div(a, b float64) (float64, error) {
if b == 0 {
return 0, fmt.Errorf("错误码: [%d] \t 除数小于0", 100)
}
return a / b, nil
}
|
调用的效果:
| fmt.Println(div(10, 0)) // 0 错误码: [100] 除数小于0
|
捕获错误
当我们写驱动模块的时候,在调用桩模块时有可能返回错误,我们可以通过判断 最后一个返回值 err (最后一个返回值返回错误类型,这是约定俗成的)来判断是否有返回错误。
| func main() {
quotient, err := div(10, 0)
if err != nil { // 捕获错误
fmt.Println(err)
return
}
fmt.Println(quotient)
}
|
自定义错误
简简单单的 Error.New() 和 fmt.Errorf() 有时候并不能满足我们的需求,所以我们还得自定义错误。
自定义错误分为三步:
- 创建一个结构体
- 实现
error 接口
- 定义其他方法
其中第一步和第二步是必选的,第三步是可选的。
Example
| // 创建一个结构体
type dataError struct {
Err string
height int
weight int
age int
}
// 实现 error 接口,使 dataError 成为 error 类型
func (d *dataError) Error() string { return d.Err }
// 定义其他方法
func (d *dataError) heightNegativeError() bool { return d.height < 0 }
func (d *dataError) weightNegativeError() bool { return d.weight < 0 }
func (d *dataError) ageNegativeError() bool { return d.age <= 0 }
|
| type status = bool
const (
SUCCESS = true // SUCCESS 验证通过
FAIL = false // FAIL 验证未通过
)
type Person struct {
height int
weight int
age int
}
// 验证不通过时,抛出错误
func variety(p Person) (status, error) {
if p.height < 0 || p.weight < 0 || p.age < 0 {
return FAIL, &dataError{"数据有误", p.height, p.weight, p.age} // 抛出错误
}
return SUCCESS, nil
}
|
| func main() {
p := Person{178, 120, -18}
s, err := variety(p)
if err != nil { // 捕获错误
fmt.Println("Error:", err)
return
}
fmt.Println(s, p)
}
|
| $ go run main.go
Error: 数据有误
|
如果捕获到了错误,而我们又想进一步判断是什么错误,需要用到 类型断言。
| func main() {
p := Person{178, 120, -18}
s, err := variety(p)
if err != nil { // 捕获错误
if ins, ok := err.(*dataError); ok { // 类型断言,判断是哪种错误
switch { // 捕获后的处理,可以打印错误信息,也可以取个绝对值等其他操作。
case ins.heightNegativeError(): ins.Err = "身高为负"
case ins.weightNegativeError(): ins.Err = "体重为负"
case ins.ageNegativeError(): ins.Err = "年龄为负"
}
fmt.Println("Error:", ins)
}
return
}
fmt.Println(s, p)
}
|
输出结果:
| $ go run main.go
Error: 年龄为负
|
异常
当一个程序遇到意料之外的错误时,应该抛出异常,停止程序的运行。
Go 中引入两个内置函数 panic() 和 recover() 来出发和终止异常处理流程,同时引入关键字 defer 来延迟执行延迟函数。
复习一下 defer
| func fn() {
fmt.Println("start")
defer fmt.Println(1)
defer fmt.Println(2)
defer fmt.Println(3)
fmt.Println("end")
}
// ---------------------
// Output:
start
end
3
2
1
|
先 defer 的后执行,后 defer 的先执行
panic
- 内置函数
- 发生
panic 的情况有两种:
- 手动抛出,即编写
panic 语句
- 程序发生错误由系统抛出。
- 如果函数中出现了
panic 语句,则不会继续执行 panic 下面的代码,而是会逆序执行 defer 修饰的函数,然后返回调用处
- 如果一路都没有遇到
recover(),则一路传递回所在协程的起点,然后该协程结束,终止其他所有协程(包括主协程)。
- 最终整个 goroutine 退出,报告错误。
panic 的签名如下:
| func panic(v interface{})
|
panic 中接受一个任何类型的参数,可以传递一个错误消息的字符串,也可以传递一个错误码。
Example
| func fn() {
fmt.Println("start")
defer fmt.Println(1)
defer fmt.Println(2)
panic(100)
defer fmt.Println(3)
fmt.Println("end")
}
func main() {
defer fmt.Println("main defer...")
fn()
fmt.Println("After fn()...")
}
// ---------------------
// Output:
start
2
1
main defer...
panic: 100
goroutine 1 [running]:
main.fn() e:/---CODE/GO/root/src/boii.xyz/study/HelloWorld/main.go:19 +0x166
main.main() e:/---CODE/GO/root/src/boii.xyz/study/HelloWorld/main.go:26 +0x27
exit status 2
|
可以看出发生 panic 之后,在 panic 之前 defer 的代码依然会逆序执行,然后回到调用处。
上面的例子执行完 fn() 中 defer 函数后,便返回了 main() 函数,
接着执行 早在调用 fn() 之前就 defer 的 打印语句,然后就终止程序了。
recover
- 内置函数
- 用来终止一个协程中的 panicking 行为,捕获
panic,从而影响应用的行为。
recover 需要用 defer 修饰。
- 一般的调用建议:
- 在 defer 函数中,通过
recover 来终止一个 协程的 panicking 过程,从而恢复正常代码的执行
- 可以获取通过
panic 传递的 error
简而言之,go中可以抛出一个 panic的异常,然后在 defer 中通过 recover 捕获这个异常,然后正常处理。
Faq
Q:为什么 recover 需要 defer 修饰?
A:因为发生 panic 后,只会执行 defer 修饰的函数,然后返回函数调用处。
而 recover 是专门用来恢复 panic 的,所以必须用 defer 修饰 recover,
且 recover 需放置在出现或可能出现 panic 的地方 之前,否则一旦发生 panic,程序控制流就开始往回走,panic 下方的任何代码包括 defer 修饰的地方也不会执行,这样就捕获不到 panic 了。
Example
发生数组下标越界
| func traverse(a [5]int) {
for i := 0; i <= 5; i++ {
fmt.Println(a[i])
}
fmt.Println("under for")
}
func main() {
defer fmt.Println("main defer...")
a := [5]int{1, 2, 3, 4, 5}
traverse(a)
fmt.Println("After div()...")
}
|
输出:
| 1
2
3
4
5
main defer...
panic: runtime error: index out of range [5] with length 5
goroutine 1 [running]:
main.traverse(0x1, 0x2, 0x3, 0x4, 0x5)
e:/---CODE/GO/root/src/boii.xyz/study/HelloWorld/main.go:17 +0xbe
main.main()
e:/---CODE/GO/root/src/boii.xyz/study/HelloWorld/main.go:25 +0x105
exit status 2
|
最后的
exit status 2说明程序是非正常退出的。
| func traverse(a [5]int) {
defer func() {
if msg := recover(); msg != nil {
fmt.Println(msg)
}
}()
for i := 0; i <= 5; i++ { // 这里会发生数组下标越界,引发panic
fmt.Println(a[i])
}
fmt.Println("under for")
}
func main() {
defer fmt.Println("main defer...")
a := [5]int{1, 2, 3, 4, 5}
traverse(a)
fmt.Println("After div()...")
}
|
输出:
| 1
2
3
4
5
runtime error: index out of range [5] with length 5
After div()...
main defer...
|
可以看到,发生数组下标越界时,程序会引发 panic,然后开始执行 defer 函数;
在 defer 函数里遇到了 recover,使得程序恢复正常运行,打印了错误信息后,回到主函数继续执行
但是我们会发现虽然被 recover 恢复了,但是 for 循环下面的 语句依然不会执行。
结论
recover 必须用 defer 修饰
recover 必须放在可能引发 panic 的地方之前
即使 recover 了,在函数里,引发 panic 的地方下面那些代码依然没有机会运行。
