Go语言高级编程-第1章 语言基础2

Go语言高级编程系列是我读《Go语言高级编程》时的一些要点总结。

1.7 错误和异常

  1. 在C语言中,默认采用一个整数类型的 errno 来表达错误,这样就可以根据需要定义多种错误类型。在Go 语言中, syscall.Errno 就是对应C语言中 errno 类型的错误。在 syscall 包中的接口,如果有返回错误的话,底层也 是 syscall.Errno 错误类型。

比如我们通过 syscall 包的接口来修改文件的模式时,如果 遇到错误我们可以通过将 err 强制断言为 syscall.Errno 错误类型来处理:

1
2
3
4
err := syscall.Chmod( ":invalid path:" ,  0666 )
if err != nil {
log.Fatal(err.(syscall.Errno))
}

在Go语言中,错误被认为是一种可以预期的结果;而异常则 是一种非预期的结果,发生异常可能表示程序中存在BUG或发生了其它不可控的问题。Go语言推荐使用 recover 函数将内部异常转为错误处理,这使得用户可以真正的关心业务相关的错误处理。

如果某个接口简单地将所有普通的错误当做异常抛出,将会使错误信息杂乱且没有价值。就像在 main 函数中直接捕获全部一样,是没有意义的:

1
2
3
4
5
6
func  main() {
defer func () {
if r:= recover();r!= nil { log.Fatal(r)
} }()
...
}

捕获异常不是最终的目的。如果异常不可预测,直接输出异常信息是最好的处理方式。

  1. 为了记录错误类型在包装的变迁过程中的信息,我们一般 会定义一个辅助的 WrapError 函数,用于包装原始的错误, 同时保留完整的原始错误类型。为了问题定位的方便,同时也 为了能记录错误发生时的函数调用状态,我们很多时候希望在 出现致命错误的时候保存完整的函数调用信息。同时,为了支 持RPC等跨网络的传输,我们可能要需要将错误序列化为类似 JSON格式的数据,然后再从这些数据中将错误解码恢出来。

为此,我们可以定义自己的 github.com/chai2010/errors 包, 里面是以下的错误类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
type  Error  interface  {
Caller() []CallerInfo
Wraped() []error
Code() int
error
private()
}

type CallerInfo struct {
FuncName string
FileName string
FileLine int
}

其中 Error 为接口类型,是 error 接口类型的扩展,用于 给错误增加调用栈信息,同时支持错误的多级嵌套包装,支持 错误码格式。为了使用方便,我们可以定义以下的辅助函数:

1
2
3
4
5
6
func  New(msg  string ) error
func NewWithCode(code int , msg string ) error
func Wrap(err error, msg string ) error
func WrapWithCode(code int,err error, msg string ) error
func FromJson(json string ) (Error, error)
func ToJson(err error) string
  1. Go语言中的错误是一种接口类型。接口信息中包含了原始类 型和原始的值。只有当接口的类型和原始的值都为空的时候, 接口的值才对应 nil 。其实当接口中类型为空的时候,原始 值必然也是空的;反之,当接口对应的原始值为空的时候,接 口对应的原始类型并不一定为空的。

因此,在处理错误返回值的时候,没有错误的返回值最好直接 写为 nil 。

Go语言作为一个强类型语言,不同类型之间必须要显式的转 换(而且必须有相同的基础类型)。但是,Go语言中

interface 是一个例外:非接口类型到接口类型,或者是接口 类型之间的转换都是隐式的。这是为了支持鸭子类型,当然会 牺牲一定的安全性。

  1. Go语言函数调用的正常流程是函数执行返回语句返回结果, 在这个流程中是没有异常的,因此在这个流程中执行 recover 异常捕获函数始终是返回 nil 。另一种是异常流程: 当函数 调用 panic 抛出异常,函数将停止执行后续的普通语句,但 是之前注册的 defer 函数调用仍然保证会被正常执行,然后 再返回到调用者。对于当前函数的调用者,因为处理异常状态 还没有被捕获,和直接调用 panic 函数的行为类似。在异常 发生时,如果在 defer 中执行 recover 调用,它可以捕获触 发 panic 时的参数,并且恢复到正常的执行流程。

在非 defer 语句中执行 recover 调用是初学者常犯的错误:

1
2
3
4
5
6
7
8
9
func main() {
if r: = recover();r != nil {
log.Fatal(r)
}
panic(123)
if r: = recover();r != nil {
log.Fatal(r)
}
}

上面程序中两个 recover 调用都不能捕获任何异常。在第一 个 recover 调用执行时,函数必然是在正常的非异常执行流 程中,这时候 recover 调用将返回 nil 。发生异常时,第 二个 recover 调用将没有机会被执行到,因为 panic 调用会 导致函数马上执行已经注册 defer 的函数后返回。

其实 recover 函数调用有着更严格的要求:我们必须在 defer 函数中直接调用 recover 。如果 defer 中调用的是 recover 函数的包装函数的话,异常的捕获工作将失败!

如果是在嵌套的 defer 函数中调用 recover 也将导致 无法捕获异常

1
2
3
4
5
6
7
8
9
10
11
func main() {
defer func() {
defer func() { // 无法捕获异常
if r: = recover();
r != nil {
fmt.Println(r)
}
}()
}()
panic(1)
}

2层嵌套的 defer 函数中直接调用 recover 和1层 defer 函 数中调用包装的 MyRecover 函数一样,都是经过了2个函数帧 才到达真正的 recover 函数,这个时候Goroutine的对应上一 级栈帧中已经没有异常信息。

必须要和有异常的栈帧只隔一个栈帧, recover 函数才能正 常捕获异常。换言之, recover 函数捕获的是祖父一级调用 函数栈帧的异常(刚好可以跨越一层 defer 函数)!

  1. 我们可以模拟出不同类型的异常。通过为定义不同类型的保护接口,我们就可以区分异常的类型
    了:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
func main {
defer func() {
if r: = recover();
r != nil {
switch x: = r.(type) {
case runtime.Error:
// 这是运行时错误类型异常 case error:
// 普通错误类型异常 default :
// 其他类型异常
}
}
}()
...
}
  1. 如果遇到要查阅API的时候可以通过godoc命令打开 自带的文档查询。Go语言本身不仅仅包含了所有的文档,也 包含了所有标准库的实现代码,这是第一手的最权威的Go语 言资料。我们认为此时你应该已经可以熟练使用Go语言了。

Go语言高级编程-第1章 语言基础2
https://nrbackback.github.io/2021/12/04/Go语言高级编程-第1章 语言基础2/
作者
John Doe
发布于
2021年12月4日
许可协议