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)
}()

(完。)