• 3. Go中panic与recover注意事项


    1. 前言

    Go 语言中两个经常成对出现的两个关键字 — panic 和 recover。这两个关键字与上一节提到的 defer 有紧密的联系,它们都是 Go 语言中的内置函数,也提供了互补的功能。

    需要说明两点

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

      • 立刻停止执行当前函数的剩余代码
      • 当前goroutine中递归执行调用 defer
    2. recover 可以中止 panic 造成的程序崩溃。它是一个只能在 defer 中发挥作用的函数,在其他作用域中调用不会发挥作用;

      • recover只能与defer结合使用

    2. 现象

    • panic 只会触发当前goroutine的defer
    • revoce 只有在defer中调用才能生效
    • panic 允许在defer中嵌套多磁调用

    2.1 跨协程失效

    首先要介绍的现象是 panic 只会触发当前 Goroutine 的延迟函数调用,通过如下所示的代码了解该现象:

    package main
    
    import (
    	"fmt"
    	"time"
    )
    
    func main() {
    	// 主线程中的defer函数并不会执行,因为子协程 panic后,主线程中的defer并不会执行
    	defer println("in main")
    
    	go func() {
    		defer println("in goroutine")
    		fmt.Println("子协程running")
    		panic("子协程崩溃")
    	}()
    
    	time.Sleep(1 * time.Second)
    }
    
    # 输出
    $ go run main.go
    子协程running
    in goroutine
    panic: 子协程崩溃
    
    goroutine 6 [running]:
    main.main.func1()
    ... 
    

    当运行这段代码时会发现 main 函数中的 defer 语句并没有执行,执行的只有当前 Goroutine 中的 defer。

    2.2 不起作用的recover

    初学 Go 语言工程师可能会写出下面的代码,在主程序中调用 recover 试图中止程序的崩溃,但是从运行的结果中也能看出,下面的程序没有正常退出。

    package main
    
    import "fmt"
    
    func main() {
    	defer fmt.Println("in main")
    	if err := recover(); err != nil {
    		fmt.Println(err)
    	}
    
    	panic("unknown err")
    }
    
    # 输出
    $ go run main.go
    in main
    panic: unknown err
    
    goroutine 1 [running]:
    main.main()
            D:/gopath/src/Go_base/lesson/panic/demo5.go:11 +0x125
    

    仔细分析一下这个过程就能理解这种现象背后的原因,recover 只有在发生 panic 之后调用才会生效。然而在上面的控制流中,recover 是在 panic 之前调用的,并不满足生效的条件,所以我们需要在 defer 中使用 recover 关键字。

    正确的写法应该是这样:

    package main
    
    import "fmt"
    
    func main() {
    	defer fmt.Println("in main")
    	defer func() {
    		if err := recover(); err != nil {
    			fmt.Println("occur error")
    			fmt.Println(err)
    		}
    	}()
    
    	panic("unknown err")
    
    }
    

    2.3 嵌套使用panic

    panic 是可以多次嵌套调用的。,如下所示的代码就展示了如何在 defer 函数中多次调用 panic:

    package main
    
    import "fmt"
    
    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]:
    main.main.func1.1()
    ...
    

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

    3. panic数据结构

    panic 关键字在源代码是由数据结构 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 是否被强行终止;

    具体的panic 程序崩溃与恢复崩溃原理在此不做延伸, 可参考panic与recover

    4. 小结

    简单总结一下程序崩溃和恢复的过程:

    1. 编译器会负责做转换关键字的工作
      • 将 panic 和 recover 分别转换成 runtime.gopanic 和 runtime.gorecover;
      • 将 defer 转换成 runtime.deferproc 函数
      • 在调用 defer 的函数末尾调用 runtime.deferreturn 函数;
    2. 在运行过程中遇到 runtime.gopanic 方法时,会从 Goroutine 的链表依次取出 runtime._defer 结构体并执行;
    3. 如果调用延迟执行函数时遇到了 runtime.gorecover 就会将 _panic.recovered 标记成 true 并返回 panic 的参数;
      • 在这次调用结束之后,runtime.gopanic 会从 runtime._defer 结构体中取出程序计数器 pc 和栈指针 sp 并调用 runtime.recovery 函数进行恢复程序;
      • runtime.recovery 会根据传入的 pc 和 sp 跳转回 runtime.deferproc;
      • 编译器自动生成的代码会发现 runtime.deferproc 的返回值不为 0,这时会跳回 runtime.deferreturn 并恢复到正常的执行流程;
    4. 如果没有遇到 runtime.gorecover 就会依次遍历所有的 runtime._defer,并在最后调用 runtime.fatalpanic 中止程序、打印 panic 的参数并返回错误码 2;

    5. 参考

    1. https://draveness.me/golang/docs/part2-foundation/ch05-keyword/golang-panic-recover/
    2. https://blog.golang.org/defer-panic-and-recover
    ♥永远年轻,永远热泪盈眶♥
  • 相关阅读:
    pycharm快捷键、常用设置、包管理
    【转载】TCP socket心跳包示例程序
    【转载】C++定时器实现
    金龙一卡通登陆爬取数据 免验证码 多线程 学生卡 CAUC
    python3 正方教务系统 爬取数据
    29、Python之Web框架Django入门
    28、Python之前端组件学习
    27、Python之jQuery基础
    26、Python之JavaScript进阶篇
    25、Python之JavaScript基础
  • 原文地址:https://www.cnblogs.com/failymao/p/15709091.html
Copyright © 2020-2023  润新知