Go

Go 异常处理

2021-07-19  本文已影响0人  王勇1024

目录

panic和recover

作用

注意事项

嵌套调用

Go 语言中的 panic 是可以多次嵌套调用的。一些熟悉 Go 语言的读者很可能也不知道这个知识点,如下所示的代码就展示了如何在 defer 函数中多次调用 panic:

func main() {
    defer fmt.Println("in main")
    defer func() {
        defer func() {
            panic("panic again and again")
        }()
        panic("panic again")
    }()

    panic("panic once")
}

$ go run main.go
in main
panic: panic once
    panic: panic again
    panic: panic again and again

goroutine 1 [running]:
...
exit status 2

从上述程序输出的结果,我们可以确定程序多次调用 panic 也不会影响 defer 函数的正常执行,所以使用 defer 进行收尾工作一般来说都是安全的。

数据结构

panic 关键字在 Go 语言的源代码是由数据结构 runtime._panic 表示的。每当我们调用 panic 都会创建一个如下所示的数据结构存储相关信息

type _panic struct {
    argp      unsafe.Pointer
    arg       interface{}
    link      *_panic
    recovered bool
    aborted   bool
    pc        uintptr
    sp        unsafe.Pointer
    goexit    bool
}
  1. argp 是指向 defer 调用时参数的指针;

  2. arg 是调用 panic 时传入的参数;

  3. link 指向了更早调用的 runtime._panic 结构;

  4. recovered 表示当前 runtime._panic 是否被 recover 恢复;

  5. aborted 表示当前的 panic 是否被强行终止;

从数据结构中的 link 字段我们就可以推测出以下的结论:panic 函数可以被连续多次调用,它们之间通过 link 可以组成链表。

defer

defer后边会接一个函数,但该函数不会立刻被执行,而是等到包含它的程序返回时(包含它的函数执行了return语句、运行到函数结尾自动返回、对应的goroutine panic)defer函数才会被执行。通常用于资源释放、打印日志、异常捕获等。

func main() {
    f, err := os.Open(filename)
    if err != nil {
        return err
    }
    /**
     * 这里defer要写在err判断的后边而不是os.Open后边
     * 如果资源没有获取成功,就没有必要对资源执行释放操作
     * 如果err不为nil而执行资源执行释放操作,有可能导致panic
     */
    defer f.Close()
}

如果有多个defer函数,调用顺序类似于栈,越后面的defer函数越先被执行(后进先出)

func main() {
    defer fmt.Println(1)
    defer fmt.Println(2)
    defer fmt.Println(3)
    defer fmt.Println(4)
}
#输出结果:
4
3
2
1

defer影响返回值

如果包含defer函数的外层函数有返回值,而defer函数中可能会修改该返回值,最终导致外层函数实际的返回值可能与你想象的不一致,这里很容易踩坑,来几个例子:

例1

func f() (result int) {
    defer func() {
        result++
    }()
    return 0
}

例2

func f() (r int) {
    t := 5
    defer func() {
        t = t + 5
    }()
    return t
}

例3

func f() (r int) {
    defer func(r int) {
        r = r + 5
    }(r)
    return 1
}

请先不要向下看,在心里跑一遍上边三个例子的结果,然后去验证
可能你会认为:例1的结果是0,例2的结果是10,例3的结果是6,那么很遗憾的告诉你,这三个结果都错了
为什么呢,最重要的一点就是要明白,return xxx这一条语句并不是一条原子指令
含有defer函数的外层函数,返回的过程是这样的:先给返回值赋值,然后调用defer函数,最后才是返回到更上一级调用函数中,可以用一个简单的转换规则将return xxx改写成

返回值 = xxx
调用defer函数(这里可能会有修改返回值的操作)
return 返回值

例1可以改写成这样

func f() (result int) {
    result = 0
    //在return之前,执行defer函数
    func() {
        result++
    }()
    return
}

所以例1的返回值是1

例2可以改写成这样

func f() (r int) {
    t := 5
    //赋值
    r = t
    //在return之前,执行defer函数,defer函数没有对返回值r进行修改,只是修改了变量t
    func() {
        t = t + 5
    }
    return
}

所以例2的结果是5

例3可以改写成这样

func f() (r int) {
    //给返回值赋值
    r = 1
    /**
     * 这里修改的r是函数形参的值,是外部传进来的
     * func(r int){}里边r的作用域只该func内,修改该值不会改变func外的r值
     */
    func(r int) {
        r = r + 5
    }(r)
    return
}

所以例3的结果是1

defer函数传参

defer函数的参数值,是在申明defer时确定下来的

在defer函数申明时,对外部变量的引用是有两种方式:作为函数参数和作为闭包引用 作为函数参数,在defer申明时就把值传递给defer,并将值缓存起来,调用defer的时候使用缓存的值进行计算(如上边的例3) 而作为闭包引用,在defer函数执行时根据整个上下文确定当前的值

func main() {
    i := 0
    defer fmt.Println("a:", i)
    //闭包调用,将外部i传到闭包中进行计算,不会改变i的值,如上边的例3
    defer func(i int) {
        fmt.Println("b:", i)
    }(i)
    //闭包调用,捕获同作用域下的i进行计算
    defer func() {
        fmt.Println("c:", i)
    }()
    i++
}
// 输出结果
c: 1
b: 0
a: 0

在defer中捕获panic消息

func main() {
    r := doPanic()
    println("Result:" + r.Message)
}

func doRecover(r *Result) {
    if err := recover(); err != nil {
    // 输出 panic 信息
        fmt.Println(err)
    // 打印调用栈
    debug.PrintStack()
        r.Message = err.(string)
    }
    println("->doRecover")
}

func doPanic() (r *Result) {
    //err := errors.New("")
    r = &Result{}
    defer doRecover(r)
    println("->doPanic")
    panic("panic")
    return r
}

type Result struct {
    Message string
}

参考文档

go defer,panic,recover详解 go 的异常处理

上一篇下一篇

猜你喜欢

热点阅读