Go 学习笔记9-Go错误处理
目录
1,error 接口
error 接口是 Go 原生内置的类型,它的定义如下:
// $GOROOT/src/builtin/builtin.go
type error interface {
Error() string
}
任何实现了 error 的 Error 方法的类型的实例,都可以作为错误值赋值给 error 接口变量。
Go 中有两个常用的函数可生成 error 类型变量:
errors.New
fmt.Errorf
使用示例:
func doSomething(...) error {
... ...
return errors.New("some error occurred")
}
判断错误的最常用方式:
err := doSomething()
if err != nil {
// 不关心err变量底层错误值所携带的具体上下文信息
// 执行简单错误处理逻辑并返回
... ...
return err
}
上面的方式,调用者并不关心具体的错误信息。
通过下面的方式可以针对不同的错误信息,做出不同的处理逻辑。
data, err := b.Peek(1)
if err != nil {
switch err.Error() {
case "bufio: negative count":
// ... ...
return
case "bufio: buffer full":
// ... ...
return
case "bufio: invalid use of UnreadByte":
// ... ...
return
default:
// ... ...
return
}
}
但是上面的方式严重依赖错误信息,一旦错误信息发生改变,调用者就得跟着改变。
Go 1.13 及后续版本,建议使用的错误判断方法:
errors.Is
方法去检视某个错误值是否就是某个预期错误值errors.As
方法去检视某个错误值是否是某自定义错误类型的实例
对于 errors.Is
方法,如果 error 类型变量的底层错误值是一个包装错误(Wrapped Error),errors.Is
方法会沿着该包装错误所在错误链(Error Chain),与链上所有被包装的错误(Wrapped Error)进行比较,直至找到一个匹配的错误为止。
例子:
var ErrSentinel = errors.New("the underlying sentinel error")
func main() {
// 用 %w 来包装错误
err1 := fmt.Errorf("wrap sentinel: %w", ErrSentinel)
err2 := fmt.Errorf("wrap err1: %w", err1)
println(err2 == ErrSentinel) // false
if errors.Is(err2, ErrSentinel) { // true
println("err2 is ErrSentinel")
return
}
println("err2 is not ErrSentinel")
}
对于 errors.As
方法,使用模式:
// 类似 if e, ok := err.(*MyError); ok { … }
var e *MyError
if errors.As(err, &e) {
// 如果err类型为*MyError,变量e将被设置为对应的错误值
}
如果 error 类型变量的动态错误值是一个包装错误,errors.As
函数会沿着该包装错误所在错误链,与链上所有被包装的错误的类型进行比较,直至找到一个匹配的错误类型,就像 errors.Is
函数那样。
type MyError struct {
e string
}
func (e *MyError) Error() string {
return e.e
}
func main() {
var err = &MyError{"MyError error demo"}
err1 := fmt.Errorf("wrap err: %w", err)
err2 := fmt.Errorf("wrap err1: %w", err1)
var e *MyError
if errors.As(err2, &e) { // true
println("MyError is on the chain of err2")
// 要特别注意这里,并不是将 err2 赋给了 e
// 而是将 err2 链上匹配上的值 err 赋给了 e
// 因此 err == e
println(e == err) // true
return
}
println("MyError is not on the chain of err2")
}
2,panic 异常
panic 指的是 Go 程序在运行时出现的一个异常情况。如果异常出现了,但没有被捕获并恢复,Go 程序的执行就会被终止。
panic 主要有两类来源:
- 来自 Go 运行时
- 通过 panic 函数主动触发
当函数 F 调用 panic 函数时,函数 F 的执行将停止。不过,函数 F 中已进行求值的 deferred 函数都会得到正常执行,执行完这些 deferred 函数后,函数 F 才会把控制权返还给其调用者。
在 Go 标准库中,大多数 panic 的使用都是充当类似断言的作用的。 在 Go 中,作为 API 函数的作者,你一定不要将 panic 当作错误返回给 API 调用者。
一个例子:
func foo() {
println("call foo")
bar()
println("exit foo")
}
func bar() {
println("call bar")
panic("panic occurs in bar")
zoo()
println("exit bar")
}
func zoo() {
println("call zoo")
println("exit zoo")
}
func main() {
println("call main")
foo()
println("exit main")
}
其输出结果如下:
call main
call foo
call bar
panic: panic occurs in bar
在整个程序中,都没有对 panic 异常进行捕捉,所以在遇到 panic 异常后,程序就退出了。
在 Go 中使用 recover 函数对 panic 异常进行捕捉。
// 在一个 defer 匿名函数中调用 recover 函数对 panic 进行捕捉
func bar() {
defer func() {
if e := recover(); e != nil {
fmt.Println("recover the panic:", e)
}
}()
println("call bar")
panic("panic occurs in bar")
zoo()
println("exit bar")
}
- recover 是 Go 内置的专门用于恢复 panic 的函数,它必须被放在一个 defer 函数中才能生效
- 如果 recover 捕捉到 panic,它就会返回以 panic 的具体内容为错误上下文信息的错误值
- 实际过程是,在 bar 中遇到 panic 后,bar 函数被异常终止
- 在 bar 退出时,要执行 defer 函数,此时 defer 函数中的 recover 捕获到了一个 pnic 异常
- 并且 recover 阻止了异常继续向上抛
- 此时,从 foo 函数的视角来看,bar 函数与正常返回没有什么差别。foo 函数依旧继续向下执行。
- 如果没有 panic 发生,那么 recover 将返回 nil
- 而且,如果 panic 被 recover 捕捉到,panic 引发的 panicking 过程就会停止
捕捉异常后的程序运行结果:
call main
call foo
call bar
recover the panic: panic occurs in bar
exit foo
exit main
将 panic 作为断言方式使用:
// $GOROOT/src/encoding/json/encode.go
func (w *reflectWithString) resolve() error {
... ...
switch w.k.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
w.ks = strconv.FormatInt(w.k.Int(), 10)
return nil
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
w.ks = strconv.FormatUint(w.k.Uint(), 10)
return nil
}
// 正常情况下,程序不会走到这里
// 如果走到了这里,说明出现了问题
// 相当于 assert 的作用
panic("unexpected map key type")
}
注意,Go 中 panic 并不同于 Java,Python 中的 raise 异常,所以不要将 panic 像 Exception 一样使用。 就是,作为 Go API 函数的作者,一定不要将 panic 当作错误返回给 API 调用者。
3,defer 函数
defer 是 Go 语言提供的一种延迟调用机制,defer 的运作离不开函数。
- 在 Go 中,只有在函数(和方法)内部才能使用 defer;
- defer 关键字后面只能接函数(或方法),这些函数被称为 deferred 函数。
- defer 将它们注册到其所在 Goroutine 中,用于存放 deferred 函数的栈数据结构中,这些 deferred 函数将在执行 defer 的函数退出前,按后进先出(LIFO)的顺序被程序调度执行。
- defer 关键字是在注册函数时对函数的参数进行求值的。
无论是执行到函数体尾部返回,还是在某个错误处理分支显式 return,又或是出现 panic,已经存储到 deferred 函数栈中的函数,都会被调度执行。所以说,deferred 函数是一个可以在任何情况下为函数进行收尾工作的好“伙伴”。
使用 defer 的一个示例:
func doSomething() error {
var mu sync.Mutex
mu.Lock()
defer mu.Unlock()
r1, err := OpenResource1()
if err != nil {
return err
}
defer r1.Close()
r2, err := OpenResource2()
if err != nil {
return err
}
defer r2.Close()
r3, err := OpenResource3()
if err != nil {
return err
}
defer r3.Close()
// 使用r1,r2, r3
return doWithResources()
}
对于自定义的函数或方法,defer 可以给与无条件的支持,但是对于有返回值的自定义函数或方法,返回值会在 deferred 函数被调度执行的时候被自动丢弃。
不是所有的内置函数都能作为 deffered 函数
- append、cap、len、make、new、imag 等内置函数都是不能直接作为 deferred 函数的
- 而 close、copy、delete、print、recover 等内置函数则可以直接被 defer 设置为 deferred 函数
对于那些不能直接作为 deferred 函数的内置函数,我们可以使用一个包裹它的匿名函数来间接满足要求,以 append 为例是这样的:
defer func() {
_ = append(sl, 11)
}()
(完。)
文章作者 @码农加油站
上次更改 2022-06-17