Go语言之错误处理和宕机
Go 语言追求简洁优雅,所以,Go 语言不支持传统的 try…catch…finally 这种处理。
Go 中引入的错误处理方式为:defer, panic, recover
程序返回错误前,需要定义会产生哪些可能的错误。在Go 语言中,使用errors 包进行错误的定义, 格式如下:
var err = errors.New("this is an error")
错误字符串由于相对固定, 一般在包作用域声明,应尽量减少在使用时直接使用errors.New 返回
1、errors 包:
Go语言中引入 error 接口类型作为错误处理的标准模式,如果函数要返回错误,则返回值类型列表中肯定包含 error。接口声明位置为系统包路径(go1.18.1为例子):
/usr/local/go/src/builtin/builtin.go //本例子中go安装在/usr/local目录下
Go语言中返回的 error 类型究竟是什么呢?查看Go语言的源码就会发现 error 类型是一个非常简单的接口类型,如下所示:
// The error built-in interface type is the conventional interface for
// representing an error condition, with the nil value representing no error.
type error interface {
Error() string
}
error 接口有一个签名为 Error() string 的方法,所有实现该接口的类型都可以当作一个错误类型。Error() 方法给出了错误的描述,在使用fmt.Println 打印错误时,会在内部调用 Error() string 方法来得到该错误的描述。只要实现了error接口就可以被error类型所接收,打印的时候会直接打印实现接口时方法返回的字符串。
一般情况下,如果函数需要返回错误,就将 error 作为多个返回值中的最后一个(但这并非是强制要求)
下面例子中,实现了Error()方法,并自定义一个错误信息,如图:

说明:
- 第13行:实现了error接口的Error()方法,方法的接收器为结构体,返回为字符串类型
- 第14行:定义了字符串,表示返回的错误提示信息
- 第19行:创建函数,返回整数类型和错误类型,方法内部执行实例化结构体
- 第26行:主函数中,调用函数,获取函数的返回值
- 第27行:判断返回的结构体内部的值(age)是否等于1,不等于提示报错信息,并打印实际的值
代码运行结果如下:

2、在代码中使用错误定义,手动定义错误
手动定义错误需要使用errors包中的New函数,函数位置为系统包路径下的errors.go文件中,如图:

下面的代码会定义一个除法函数,当除数为0 时,返回一个预定义的除数为0 的错误,如下:

说明:
- 第9行:通过New函数预定义一个除数为0的错误
- 第11行:创建运算函数,参数为被除数和除数,返回类型为整型和错误类型
- 第12行,进行判断,如果被除数为0,那么返回0和预定义的错误
- 第16行,表示没有异常情况下正常执行运算,返回结果和nil
- 第19行:主函数中调用运算函数,传递两个参数,此时除数为0,函数返回的两个值对应b和c
- 第20行:如果除数为0,那么返回值0对应b,c对应自定义的错误,因此判断b等于0是,打印c
- 第22行:如果除数不为0,那么nil对应c,correctResult对应b,此时判断c,打印b
- 第25行:打印自定义错误字段变量的类型
代码执行结果如下:

注意:如果error没有错误返回,可直接返回nil
3、设置错误的格式:
除errors包外,标准库中的fmt 包还提供了方法Errorf,可用于设置返回的错误字符串的格式,将多个值合并成更有意义的错误字符串, 从而动态地创建错误字符串,如图:

运行后,打印如下错误字符串:

4、从函数返回错误:
下面例子表示调用函数时返回错误,在主函数中处理错误,如图:


Go 语言错误处理方式的一个优点:错误处理不是在函数中,而是在调用函数的地方进行的,在错误处理方面提供了极大的灵活性
5、异常的捕获
使用defer + recover 来捕获和处理异常(返回错误变量)
下面的例子捕获异常,异常为除数不能为0,如图:

- 第6行:通过defer+recover语句,后面加匿名函数
- 第7行:recover()为内置函数,可以捕获到异常
- 第8行:判断如果错误不为nil,那么打印err
- 第12-13行:声明变量并赋值
- 第14行:执行除法运算,声明变量result并将结果赋值给result
- 第19行:调用函数打印结果
代码运行结果如下:

从上图中的错误可知:提示除数不能为0,arr1的值为10,arr2的值为0,recover()捕获到了此异常,并打印出来
宕机(panic)–程序终止运行
宕机不是一件很好的事情, 可能造成体验停止、服务中断,但是,如果在损失发生时,程序没有因为宕机而停止, 那么用户将会付出更大的代价,因此, 看机有时是一种合理的止损方法。
Go 语言可以在程序中手动触发宕机,让程序崩愤, 这样开发者可以及时地发现错误,同时减少可能的损失。
Go 语言程序在宕机时,会将堆栈和goroutine 信息输出到控制台, 所以宕机也可以方便地知晓发生错误的位置
1、手动触发宕机:
下面例子表示手动触发宕机,如图:

- 第8行:定义函数,返回类型为错误类型
- 第11行:判断变量arr2的值是否为0,如果是0 ,抛出自定义的异常
- 第14行:如果arr2不为0的情况下,正常执行运算
- 第16行:如果正常运行,那么函数返回nil
- 第20行:定义panic(),参数为函数类型,如果出现错误,将触发宕机
panic()的参数可以是任意类型
代码执行结果如下:

手动宕机进行报错的方式不是一种偷懒的方式,反而能迅速报错,终止程序继续运行,防止更大的错误产生。不过,如果任何错误都使用宕机处理,也不是一种良好的设计。因此应根据需要来决定是否使用宕机进行报错。
2、在宕机时触发延迟执行语句:
当panic()触发的宕机发生时, panic()后面的代码将不会被运行,但是在panic()函数前面已经运行过的defer 语句依然会在宕机发生时发生作用
例:修改上面的语句,添加defer语句,如图:

运行结果如下:

宕机前, defer 语句会优先被执行
宕机恢复(recover)–防止程序奔溃
无论是代码运行错误由Runtime 层抛出的panic 崩渍,还是主动触发的panic 崩溃,都可以配合defer 和recover 实现错误捕捉和恢复,让代码在发生崩溃后允许继续运行。panic的参数类型是interface{},因此可以接受任意值,recover()的类型也是interface{},因此也可以接受任意值
1、让程序在崩溃时继续执行:
下面的代码实现了ProtectRun()函数,该函数传入一个匿名函数或闭包后的执行函数,当传入函数以任何形式发生panic 崩溃后,可以将崩溃发生的错误打印出来,同时允许后面的代码继续运行,不会造成整个进程的崩溃。
下面例子表示奔溃时会继续运行,如图:


- 第8行:声明描述错误的结构体,成员保存错误的执行函数
- 第13行:使用defer 将闭包延迟执行,当panic 触发崩溃时, ProtectRun()函数将结束运行,此时defer 后的闭包将会发生调用。
- 第14行:recover()获取到panic 传入的参数
- 第15行:使用switch 对err变量进行类型断言
- 第16行:如果错误是有Runtime 层抛出的运行时错误,如空指针访问、除数为0等情况,打印运行时错误。
- 第18行:其他错误, 打印传递过来的错误数据。
- 第28行:使用panic 手动触发一个错误, 井将一个结构体附带信息传递过去,此时,recover 就会获取到这个结构体信息,并打印出来。
- 第36行:模拟代码中空指针赋值造成的错误,此时会由Runtime 层抛出错误,被ProtectRun()函数的recover()函数捕获到。
2、在panic期间,延迟函数可正常执行
在panic期间,任何延迟的函数调用都将完成。因此,可以在一个单独的函数中放置一个recover调用,并在引发panic的代码之前使用defer调用该函数

运行结果如下:

从上图看出,第12行没有打印出来,只有第16行打印了,因为产生panic的函数将立即返回,而该函数块中panic之后的任何代码都不会执行。但是,在产生panic的函数返回之后,正常的执行将恢复,因此main函数中test1()后面的将继续执行
3、panic值从recover中返回
如果没有出现panic,直接打印recover()将会得到nil,但是当出现panic时,recover返回传递给panic的任何值
下面例子打印recover()的值,如图:

运行结果如下:

4、panic中传递err后调用Error()报错
虽然panic和recover的参数都是空接口,可以接受任意类型的值,但是如果传递了error后,此时的参数是interface{},是不能直接调用Error()的,因此需要将其通过类型断言为error后才可调用,如图:


panic 和recover 的关系:
- 有panic 没recover , 程序岩机
- 有panic 也有recover 捕获,程序不会宕机机。执行完对应的defer 后,从宕机机点退出,当前函数后继续执行。
虽然panic/recover 能模拟其他语言的异常机制,但并不建议编写普通函数也经常性使用这种特性。在panic 触发的defer 函数内,可以继续调用panic ,进一步将错误外抛直到程序整体崩溃。如果想在捕获错误时设置当前函数的返回值,可以对返回值使用命名返回值方式直接进行设置
仅当程序无法从错误中恢复时,才使用panic 。panic 是没有办法的办法,仅当让程序崩溃是最负责任的选择时才应使用panic


