6

Go 学习笔记9-Go错误处理

 1 year ago
source link: https://codeshellme.github.io/2022/06/go9/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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)

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK