本文的内容建立在已对 C 和 C++ 熟练掌握的基础之上,主要给出 Go 与 C 和 C++ 之间的不同之处。
Go 中的错误处理主要使用 error
接口或是 errors
包。
Go 标准库定义了 error 接口,如下。
1 | type error interface { |
任何实现了 Error()
方法的自定义类型都被认为实现了该接口,该接口返回一个 string
字符串,用于描述错误。
以除法为例,下面是一个使用 error
接口返回除法错误的实例。
1 | type DivError struct { |
在这个实例中,我们首先创建了一个除错误类,然后对该类实现了一个方法 Error()
,也就实现了 error
接口。接下来,除法函数的返回值除了正常的结果还添加了一个 error
接口类型的返回值,这里的返回值可以接收实现了 error
接口的自定义类型的值。注意一个细节,这里的 Println(err)
直接输出了错误信息,而不需要调用 err.Error()
,这是因为 Go 中的 error
会自动调用 Error()
,因此不需要显式调用。
errors
包是 Go 提供用于处理错误的包,其基本功能 errors.New()
返回包含有错误文本信息、实现了 error
接口的一个对象的指针,示例如下。
1 | err := errors.New("this is an error") // errors.New() return error |
errors
包的底层逻辑是定义了一个 errorString
结构体,并实现了 Error()
函数,再通过 New()
返回该 error
对象,如下。
1 | type errorString struct { |
除了错误本身之外,Go 还提供 fmt.Errorf()
包裹错误,进一步为错误提供上下文信息,使用示例如下。
1 | err := errors.New("file not found") |
上面的代码假设了一个文件打开的场景,其通过 fmt.Errorf()
额外为 err
提供了文件本身的描述,这里需要用到包裹错误专用的 %w
。
fmt.Errorf()
返回一个 fmt.wrapError
类型的指针,结构体包含有输出的信息,和所包裹的 error
对象。
1 | type wrapError struct { |
*fmt.wrapError
也实现了 Error()
函数,因此实现了 error
接口。fmt.Errorf(format, ...)
本质上是 errors.New(fmt.Sprintf(format, ...))
,如果 format
中有 %w
,则 err != nil
,否则的话单纯的只是给出一个格式化错误字符串,并不包裹错误。
Go 也提供了错误解包函数 errors.Unwrap()
,由于包裹错误时存有 error
对象,所以这里实际作用是取出该对象。沿用上面的例子,拓展一下,错误解包使用方法如下。
1 | err = errors.Unwrap(wraperr) |
对于多个错误,可以使用 errors.Join()
函数将多个非空错误进行组合。
1 | err1 := errors.New("this is error1") |
组合后的错误类型为 joinError
结构体,实现如下。
1 | type joinError struct { |
所以组合错误的实质是将非空的错误存储入 error
切片,joinError
实现了 Error()
函数,所以也实现了 error
接口。组合后的错误也可以被包裹。
Go 中并没有提供组合错误的分解方法,不过 joinError
本身实现了一个 Unwrap()
函数,返回错误切片,将上面错误组合的代码拓展,示例如下。
1 | errs := joinerr.(interface{ Unwrap() []error }).Unwrap() |
这里有一个细节,joinerr
此时为 error
接口,无法直接调用 Unwrap()
函数。第一个思路是使用类型断言为 joinError
,但是这个结构体类型是非导出的,所以此路不通。第二个思路是使用类型断言为 interface { Unwrap() []error }
实现调用 Unwrap()
函数。实际上,在源码 /src/fmt/errors_test.go
中有相同代码实现了该功能,但是该函数并不是可导出的。
go 的源码结构清晰,注释明确,非常易于阅读,建议多看,加深对各类功能实现的理解。例如,这里
joinError
的源码位于/src/errors/join.go
中,包含有结构体与函数实现。
errors.Is(err, target)
用于判断 err
是否等同于 target
,或是 err
包裹了 target
。
1 | err1 := errors.New("this is an error") |
注意,由于 errors.New()
返回的是指针,在比较时比较的因此也是指针。若两个错误的信息字符串一致但错误的地址不同,那么他们是不同的 error
,例如下面这个例子。
1 | err1 := errors.New("this is an error") |
errors.As(err, target)
的主要目的是为了从 err
中提取特定为 target
类型的错误,当在 err
中找到对应错误时,target
会直接指向错误。下面是一个示例,假设我们已经用 err2
包裹了错误 err1
。
1 | type MyError struct { |
输出的结果如下。
1 | myerror 1 |
注意,在代码中,为了修改 target
指针的指向,需要传址。同时在结果中可以看到,target
正确指向了包裹错误中的 MyError
类型错误的地址。
Go 中的 panic
为严重错误,在 panic
发生时,程序会中断执行并崩溃,输出堆栈等错误信息。
Go 中的 panic
可以手动触发,触发示例及输出如下。
1 | func main() { |
panic()
中除了可以填充 string
外还可以填充 error
。
defer
用于一个函数的延迟调用,有 defer
修饰的语句会在其返回时调用。defer
语句具体的执行时机为:return
值确定之后,执行 defer
,再执行 return
。多个 defer
在函数中返回时表现为先进后出,类似于栈,示例如下。
1 | func main() { |
由于 defer
退出时执行的特点,在 Go 中,我们可以使用 defer
的函数对 panic
进行捕获,避免程序整体出现崩溃。其本质是 panic
会在触发时向上回溯栈中的 defer
并执行,如果 defer
中存在有 recover()
,那么 recover()
会捕获 panic
,中止 panic
向上传播。
下面是一个 recover()
捕获 panic
的示例。
1 | func catchpanic() { |
上面这种写法一般不推荐,因为涉及到调用函数产生额外开销,建议使用匿名函数应对局部 panic
,上面的例子可以等效为下面的代码。
1 | func main() { |
这里注意一个小细节,使用匿名函数时 defer func() {}()
的最后一个括号表示执行,否则只是定义而没有使用。
panic
和 error
区别:panic
一般用于严重错误以及不可预知的异常,而 error
则是可以预见的错误,可提前处理。例如,打开文件时,文件不存在是一种可以预见的错误,可以提前在代码中判断处理。