本文的内容建立在已对 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 则是可以预见的错误,可提前处理。例如,打开文件时,文件不存在是一种可以预见的错误,可以提前在代码中判断处理。