阅读目录
一、error 类型及其使用
Go 语言错误处理机制
Go 语言错误处理机制非常简单明了,不需要学习了解复杂的概念、函数和类型,Go 语言为错误处理定义了一个标准模式,即 error
接口,该接口的定义非常简单:
1 2 3 |
|
其中只声明了一个 Error()
方法,用于返回字符串类型的错误消息。对于大多数函数或类方法,如果要返回错误,基本都可以定义成如下模式 —— 将错误类型作为第二个参数返回
1 2 3 |
|
然后在调用返回错误信息的函数/方法时,按照如下「卫述语句」模板编写处理代码即可:
1 2 3 4 5 6 7 |
|
返回错误实例并打印
Go 标准错误包 errors
提供的 New()
方法快速创建一个 error
类型的错误实例
1 2 3 4 5 6 7 8 9 10 |
|
参照上面介绍的 Go 错误处理标准模式,调用这个函数并编写错误处理的代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
为了方便测试,我们将通过命令行参数传递 add
函数的参数,这里我们引入了 os
包读取命令行参数,并通过 strconv
包提供的 Atoi
方法将其转化为整型(命令行读取参数值默认是字符串类型,转化时忽略错误以便简化处理流程),然后分别赋值为 x
、y
变量,再调用 add
函数进行运算。
注意到我们在打印错误信息时,直接传入了 err
对象实例,因为 Go 底层会自动调用 err
实例上的 Error()
方法返回错误信息并将其打印出来,就像普通类的 String()
方法一样。
我们简单测试下不传递参数、传递错误类型参数和传递正常参数这几种场景,打印结果如下:
以上这种错误处理已经能够满足我们日常编写 Go 代码时大部分错误处理的需求了,事实上,Go 底层很多包进行错误处理时就是这样做的。此外,我们还可以通过 fmt.Errorf()
格式化方法返回 error
类型错误,其底层调用的其实也是 errors.New
方法:
1 2 3 |
|
更复杂的错误类型
系统内置错误类型
除了上面这种最基本的、使用 errors.New()
方法返回包含错误信息的错误实例之外,Go 语言内置的很多包还封装了更复杂的错误类型。
以 os 包为例,这个包主要负责与操作系统打交道,所以提供了 LinkError
、PathError
、SyscallError
这些实现了 error
接口的错误类型,以 PathError
为例,顾名思义,它主要用于表示路径相关的错误信息,比如文件不存在,其底层类型结构信息如下:
1 2 3 4 5 |
|
该错误类型除了组合 error
接口实现 Error()
方法外,还提供了额外的操作类型字段 Op
和文件路径字段 Path
以丰富错误信息,方便定位问题,该类型的 Error()
方法实现如下:
1 2 3 |
|
我们可以在调用 os
包方法出错时通过 switch
分支语句判定具体的错误类型,然后进行相应的处理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
自定义错误类型
我们也可以仿照 PathError
的实现自定义一些复杂的错误类型,只需要组合 error
接口并实现 Error()
方法即可,然后按照自己的需要为自定义类型添加一些属性字段,这很简单,就不展开介绍了。
小结
可以看到,Go 语言的错误和其他语言的错误和异常不同,它们就是从函数或者方法中返回的、和其他返回值并没有什么区别的普通 Go 对象而已,如果程序出错,要如何处理程序下一步的动作,是退出程序还是警告后继续执行,决定权完全在开发者手上。
二、defer 语句及其使用
Go 语言中的类没有构造函数和析构函数的概念,处理错误和异常时也没有提供 try...catch...finally
之类的语法,那当我们想要在某个资源使用完毕后将其释放(网络连接、文件句柄等),或者在代码运行过程中抛出错误时执行一段兜底逻辑,要怎么做呢?
通过 defer
关键字声明兜底执行或者释放资源的语句可以轻松解决这个问题。比如我们看 Go 内置的 io/ioutil 包提供的读取文件方法 ReadFile
实现源码,其中就有 defer
语句的使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
defer
修饰的 f.Close()
方法会在函数执行完成后或读取文件过程中抛出错误时执行,以确保已经打开的文件资源被关闭,从而避免内存泄露。如果一条语句干不完清理的工作,也可以在 defer
后加一个匿名函数来执行对应的兜底逻辑:
1 2 3 |
|
另外,一个函数/方法中可以存在多个 defer
语句,defer
语句的调用顺序遵循先进后出的原则,即最后一个 defer
语句将最先被执行,相当于「栈」这个数据结构,如果在循环语句中包含了 defer
语句,则对应的 defer
语句执行顺序依然符合先进后出的规则。
由于 defer
语句的执行时机和调用顺序,所以我们要尽量在函数/方法的前面定义它们,以免在后面编写代码时漏掉,尤其是运行时抛出错误会中断后面代码的执行,也就感知不到后面的 defer
语句。
下面我们看一段简单的 defer
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
在这段代码中,我们定义了两个 defer
语句,并且是在函数最顶部,以确保异常情况下也能执行。
在函数正常执行的情况下,这两个 defer
语句会在最后一条打印语句执行完成后先执行第二条 defer
语句,再执行第一条 defer
语句
而如果我们把 j
的值设置为 0
,则函数会抛出 panic:
表示除数不能为零。这个时候,由于 defer
语句定义在抛出 panic 代码的前面,所以依然会被执行,底层的逻辑是在执行 var k = i / j
这条语句时,遇到除数为 0,则抛出 panic,然后立即中断当前函数 main
的执行(后续其他语句都不再执行),并按照先进后出顺序依次执行已经在当前函数中声明过的 defer
语句,最后打印出 panic 日志及错误信息
总结一下,Go 语言的 defer
语句相当于 Java/PHP 中的析构函数和 finally
语句的功效,常用于定义兜底逻辑,在函数执行完毕后或者运行抛出 panic 时执行,如果一个函数定义了多个 defer
语句,则按照先进后出的顺序执行
三、panic 和 recover
Go 语言通过 error 类型统一进行错误处理,但这些错误都是我们在编写代码时就已经预见并返回的,对于某些运行时错误,比如数组越界、除数为0、空指针引用,这些 Go 语言是怎么处理的呢?
panic
除了像上面演示的那样由 Go 语言底层抛出 panic,我们还可以在代码中显式抛出 panic,以便对错误和异常信息进行自定义,仍然以上篇教程除数为 0 的示例代码为例,我们可以这样显式返回 panic 中断代码执行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
这样一来,当我们执行这段代码时,就会抛出 panic:
panic
函数支持的参数类型是 interface{}
:
1 |
|
所以可以传入任意类型的参数:
1 2 |
|
无论是 Go 语言底层抛出 panic,还是我们在代码中显式抛出 panic,处理机制都是一样的:当遇到 panic 时,Go 语言会中断当前协程(即 main
函数)后续代码的执行,然后执行在中断代码之前定义的 defer
语句(按照先入后出的顺序),最后程序退出并输出 panic 错误信息,以及出现错误的堆栈跟踪信息,也就是下面红框中的内容
第一行表示出问题的协程,第二行是问题代码所在的包和函数,第三行是问题代码的具体位置,最后一行则是程序的退出状态,通过这些信息,可以帮助你快速定位问题并予以解决。
recover
此外,我们还可以通过 recover()
函数对 panic 进行捕获和处理,从而避免程序崩溃然后直接退出,而是继续可以执行后续代码,实现类似 Java、PHP 中 try...catch
语句的功能。
由于执行到抛出 panic 的问题代码时,会中断后续其他代码的执行,所以,显然这个 panic 的捕获应该放到 defer
语句中完成,才可以在抛出 panic 时通过 recover
函数将其捕获,defer 语句执行完毕后,会退出抛出 panic 的当前函数,回调调用它的地方继续后续代码的执行。
下面我们引入 recover()
函数来重构上述示例代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
如果没有通过 recover()
函数捕获 panic 的话,程序会直接崩溃退出,并打印错误和堆栈信息:
而现在我们在 divide()
函数的 defer
语句中通过 recover()
函数捕获了 panic,并打印捕获到的错误信息,这个时候,程序会退出 divide()
函数而不是整个应用,继续执行 main()
函数中的后续代码,即恢复后续其他代码的执行
如果在代码执行过程中没有抛出 panic,比如我们把 divide()
函数中的 j
值改为 1,则代码会正常执行到函数末尾,然后调用 defer
语句声明的匿名函数,此时 recover()
函数返回值为 nil,不会执行 if 分支代码,然后退出 divide()
函数回到 main()
函数执行后续代码:
这样一来,当程序运行过程中抛出 panic 时我们可以通过 recover()
函数对其进行捕获和处理,如果没有抛出则什么也不做,从而确保了代码的健壮性