Go 异常
error 接口
error 基础使用
Go 语言引入了一个关于错误处理的标准模式,即 error 接口,它是 Go 语言内建的接口类型,该接口的定义如下:
type error interface {
Error() string
}
由于 Go 的函数支持多返回值,所以一般会用 error 作为其中一个返回值,代表该函数执行过程中或者逻辑有出错。
通常我们会使用 errors.New() 或者 fmt.Errorf() 来返回一个 error 对象,但注意,通过这两种方式返回的 error 对象都是不可以进行比较的,因为 errors.New() 返回的其实是一个地址,不能用来做等值判断,而 fmt.Errorf() 的内部其实也是用到了 errors.New() 。
示例:
package main
import (
"errors"
"fmt"
)
func div(num1, num2 int) (int, error) {
if num2 <= 0 {
// 也可以用 fmt.Errorf()
return -1, errors.New("num2 is not a positive number")
}
return num1 / num2, nil
}
func main() {
result, err := div(9, 0)
if err != nil {
fmt.Println("div err:", err)
}
fmt.Println("div result:", result)
err2 := errors.New("go")
err3 := errors.New("go")
// 比较的是地址
fmt.Println(err2 == err3) // false
// 可以通过Error()方法拿到其中的error字符串信息,比较异常信息字符串
fmt.Println(err2.Error() == err3.Error()) // true
}
自定义 error 对象
Go 语言内置的 error 创建方法只返回了错误信息,但很多时候我们业务上还需要错误码,即 error code ,所以我们还可以自定义 error 对象。
package main
import "fmt"
// 自定义error类
type MyError struct {
code int
msg string
}
// 对象定义完之后,接下来只需要实现error接口的Error方法即可
// 这样,我们就自定义了一个同时带有错误码和错误信息的error对象
func (e MyError) Error() string {
return fmt.Sprintf("code: %d, msg: %s", e.code, e.msg)
}
// 自定义error对象的创建函数
func NewError(code int, msg string) error {
return MyError{code: code, msg: msg}
}
// 错误错误码函数
func Code(e error) int {
if e, ok := e.(MyError); ok {
return e.code
}
return -1
}
// 错误错误信息函数
func Msg(e error) string {
if e, ok := e.(MyError); ok {
return e.msg
}
return "internal error"
}
func main() {
err := NewError(100, "test error")
fmt.Printf("code: %d, msg: %s", Code(err), Msg(err))
}
defer 关键字
defer 基础使用
defer(延迟)是 Go 语言中的一个关键字,主要用在函数或方法前面,作用是用于函数和方法的延迟调用。在语法上,defer 与普通的函数调用没有什么区别,在使用上也非常简单,只需要弄清楚以下几点即可:
-
延迟的函数的什么时候被调用?
- 函数 return 的时候
- 发生 panic 的时候
-
延迟调用的语法规则
- defer 关键字后面表达式必须是函数或者方法调用
- 延迟内容不能被括号括起来
defer 执行顺序:
当一个函数中有多个 defer 的时候,defer 语句的执行顺序是先进后出 LIFO
,即先调用的后执行。
package main
import "fmt"
func test1() {
fmt.Println("test1")
}
func test2() {
fmt.Println("test2")
}
func test3() {
fmt.Println("test3")
}
func err(num int) int {
fmt.Println("test err")
return 1 / num
}
func main() {
defer test1()
defer test2()
defer err(0)
defer test3()
}
运行结果:即使函数或某个延迟调用发生错误,这些调用依旧会被执⾏。
test3
test err
test2
test1
panic: runtime error: integer divide by zero
goroutine 1 [running]:
main.err(0x0, 0x404d5e)
E:/GO_PROJECT/PRACTISE/src/main.go:19 +0x11f
main.main()
E:/GO_PROJECT/PRACTISE/src/main.go:28 +0x8d
defer 与匿名函数结合使用
示例1:无入参的匿名函数
func main() {
a := 10
b := 20
defer func() {
fmt.Println("匿名函数中a=", a)
fmt.Println("匿名函数中b=", b)
}()
a = 100
b = 200
fmt.Println("main函数中a=", a)
fmt.Println("main函数中b=", b)
}
运行结果:
main函数中a= 100
main函数中b= 200
匿名函数中a= 100
匿名函数中b= 200
defer 会延迟函数的执行,虽然立即调用了匿名函数,但是该匿名函数不会执行,等整个 main() 函数结束之前,再去调用执行匿名函数,所以输出结果如上所示
示例2:有入参的匿名函数
func main() {
a := 10
b := 20
defer func(a, b int) {
fmt.Println("匿名函数中a=", a)
fmt.Println("匿名函数中b=", b)
}(10, 20)
a = 100
b = 200
fmt.Println("main函数中a=", a)
fmt.Println("main函数中b=", b)
}
运行结果:
main函数中a= 100
main函数中b= 200
匿名函数中a= 10
匿名函数中b= 20
当执行到匿名函数时,虽然没有立即调用执行匿名函数,但是已经完成了参数的传递
defer 使用场景
defer 关键字一般用于两个场景:资源释放、配合 recover 处理 panic
1)资源释放
通过 defer 延迟调用机制,我们可以简洁优雅地处理资源回收问题,从而避免在复杂的代码逻辑情况下,遗漏相关的资源回收问题。用的比较多的就是类似网络连接、数据库连接、文件句柄等资源释放。
func CopyFile(dstFile, srcFile string) (wr int64, err error) {
src, err := os.Open(srcFile)
if err != nil {
return
}
defer src.Close()
dst, err := os.Create(dstFile)
if err != nil {
return
}
defer dst.Close()
wr, err = io.Copy(dst, src)
return err
}
只要我们正确打开了某个资源,比如 src 和 dst,没返回 err 的情况下,都可以用 defer 延迟调用来关闭资源,这是 Go 语言中非常常见的一种资源关闭方式。
2)配合 recover 处理 panic
defer 另一个常用的地方就是在处理程序 panic 时(Go 语言中用 panic 来抛出异常,用 recover 来捕获异常)。
所以当程序出现异常而我们又需要知道是发生了什么异常时,就可以用 defer 和 recover 来捕获异常。
func main() {
defer func() {
if r := recover(); r != nil {
fmt.Println(r)
}
}()
a := 1
b := 0
fmt.Println("result:", a/b)
}
运行结果:
runtime error: integer divide by zero
可以看到,程序并没有输出 result,这是因为我们尝试对一个除数为 0 的数做除法,这是不允许的,所以程序回 panic,但我们用 defer 在程序发生 panic 的时候捕获了这个异常,所以打印出异常信息:runtime error: integer divide by zero。
defer 与 return
在 return 的时候,defer 具体做了什么?又会带来什么结果?这是一个非常值得探讨的问题,通过这个问题可以看出对 Go 语言掌握得扎不扎实。
示例1:
func deferFunc() {
var num = 1
defer fmt.Printf("num is %d", num)
num = 2
return
}
func main() {
deferFunc() // 1
}
为什么结果是 1 ?
因为延迟函数 defer fmt.Printf("num is %d", num) 的参数 num 在 defer 语句出现的时候就已经确定 num=1,所以不管后面怎么修改 a 的值,最终调用 defer 函数传递给 defer 函数的参数已经固定是 1 了,不会再变化。
示例2:
func deferFunc() {
arr := [4]int{1, 2, 3, 4}
defer PrintArr(&arr)
arr[0] = 10
return
}
func PrintArr(arr *[4]int) {
fmt.Println(arr)
}
func main() {
deferFunc() // &[10 2 3 4]
}
为什么 arr[0] 是 10 ?
我们知道在 defer 出现的时候,参数已经确定,但由于这里传递的是地址,地址没变,但是地址对应的内容被修改了,所以输出会被修改。
示例3:
func deferFunc() (res int) {
num := 1
defer func() {
res++
}()
return num
}
func main() {
fmt.Println(deferFunc()) // 2
}
为什么结果是 2 ?
这是一个非常经典的例子,要想准确地知道程序的执行结果,需要我们对函数 return 的执行有一个细致的了解。其实函数的 return 并非一个原子操作,return 的过程可以被分解为以下三步:
- 设置返回值
- 执行 defer 语句
- 将结果返回
所以,在本例中,第一步是将 res 的值设置为 num,此时还未执行 defer,num 的值是1,所以 res 被设置为 1;然后再执行 defer 语句将 res+1,最终将 res 返回,所以结果打印出 2 。
示例4:
func deferFunc() int {
num := 1
defer func() {
num++
}()
return num
}
func main() {
fmt.Println(deferFunc()) // 1
}
为什么结果是 1 ?
本例和前面的区别是返回值是匿名的,但同样可以运用上面的思路,假设返回值为 res,运用前面的思路分析,第一步将 res 设置为 num,此时 num=1,所以res=1,第二步执行 defer 将 num+1,但 res 仍是 1,第三步将 res 返回,所以最终结果是 1 。
示例5:
func deferFunc() (res int) {
num := 1
defer func() {
num++
}()
return num
}
func main() {
fmt.Println(deferFunc()) // 1
}
不难分析运行结果还是 1,同样的三步分析法,因为 defer 改变的是 num 的值,而不是改变的 res 的值,所以结果不会变(若 defer 函数里变为 res++,那么结果就会是 2 了)。
总结:当碰到 defer 与 return 时如何确定最终的返回值?
-
defer 定义的延迟函数的参数在 defer 语句出时就已经确定下来了
-
return 不是原子级操作,其执行过程是: 设置返回值 —> 执行 defer 语句 —> 将结果返回
recover
recover 基础使用
异常其实就是指程序运行过程中发生了 panic,为了不让程序 core,可以在程序中加入 recover 机制,将异常捕获,打印出异常,这样也方便我们定位错误。
而 Go 语言为我们提供了专用于“拦截”运行时 panic 的内建函数 recover(),它可以使得当前的程序从运行时 panic 的状态中恢复并重新获得流程控制权。
注意:recover 只有在 defer 调用的函数中有效
。
func main() {
defer func() {
if err := recover(); err != nil {
fmt.Println("出现了panic,使用reover获取信息:", err)
}
}()
fmt.Println("before")
panic("出现panic")
fmt.Println("after")
}
运行结果:
before
出现了panic,使用reover获取信息: 出现panic
结果分析:
- 在有了 recover 后,程序不会在 panic 初中断,而是在执行完 panic() 之后,执行 defer recover() 函数,并且当前函数 panic() 后面的代码不会再执行,但是调用该函数的代码会接着执行。
- 如果我们在 main 函数中未加入 defer func(){...} 时,程序运行到第 8 行就会 panic 掉,而通常在我们的业务程序中对于程序 panic 是不可容忍的,我们需要程序健壮运行,而不是因为一些 panic 导致挂掉又被拉起,所以当发生 panic 的时候我们要让程序能够继续运行,并且获取到发生 panic 的具体错误,就可以用上述方法。
panic 传递
当一个函数发生了 panic 之后,若在当前函数中没有 recover,会一直向外层传递直到主函数,如果迟迟没有 recover 的话,那么程序将终止。如果在过程中遇到了最近的 recover,则将被捕获。
func testPanic1() {
fmt.Println("testPanic1 before")
testPanic2()
fmt.Println("testPanic1 after")
}
func testPanic2() {
defer func() {
recover()
}()
fmt.Println("testPanic2 before")
testPanic3()
fmt.Println("testPanic2 after")
}
func testPanic3() {
fmt.Println("testPanic3 before")
panic("testPanic3 panic")
fmt.Println("testPanic3 after")
}
func main() {
fmt.Println("程序开始")
testPanic1()
fmt.Println("程序结束")
}
调用解析:
- 调用链:main--> testPanic1 --> testPanic2 --> testPanic3,但在 testPanic3 中发现了一个 panic,由于 testPanic3 没有 recover,于是向上找,在 testPanic2 中找到了 recover,panic 被捕获了,程序接着运行。
- 由于 testPanic3 发生了 panic,所以不再继续运行,函数跳出返回到 testPanic2,testPanic2 中捕获到了 panic,也不会再继续执行,跳出函数 testPanic2,最终到了 testPanic1 接着运行。
结论:
-
recover() 只能恢复当前函数级或以当前函数为首的调用链中的函数中的panic(),恢复后正调用的当前函数结束,但是调用此函数的函数继续执行。
-
函数发生了 panic 之后会一直向上传递,如果直至 main 函数都没有 recover(),程序将终止,如果碰见了 recover(),则会被 recover 捕获。
recover 捕获次数
一个 recover 只能捕获一次 panic ,且一一对应。
func main() {
defer func() {
if e := recover(); e != nil {
fmt.Printf("recover:%v\n", e)
}
}()
panic("panic1")
panic("panic2")
fmt.Println("111") // 发生panic,不会打印
}
运行结果:
recover:panic1
可以看到,程序总共两次 panic,但是第二次不会执行,因为 recover 捕获到第一个 panic 之后,该函数就退出了。