美文网首页Go
Go 异常处理

Go 异常处理

作者: 王勇1024 | 来源:发表于2021-07-19 10:04 被阅读0次

    目录

    panic和recover

    作用

    • panic 能够改变程序的控制流,调用 panic 后会立刻停止执行当前函数的剩余代码,并在当前 Goroutine 中递归执行调用方的 defer;

    • recover 可以中止 panic 造成的程序崩溃。它是一个只能在 defer 中发挥作用的函数,在其他作用域中调用不会发挥作用;

    注意事项

    • panic 只会触发当前 Goroutine 的 defer;

    • recover 只有在 defer 中调用才会生效;

    • panic 允许在 defer 中嵌套多次调用;

    嵌套调用

    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 的异常处理

    相关文章

      网友评论

        本文标题:Go 异常处理

        本文链接:https://www.haomeiwen.com/subject/hwecmltx.html