美文网首页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 的异常处理不同于其他语言使用的结构 对于 go 的异常处理, 使用 defer, panic, recove...

  • go 异常处理

    error 是一个接口,接口中有Error()方法和返回值string // 就相当于 var tmp error...

  • Go异常处理

    想法 Go的异常处理是比较特别的.有人不喜欢这种走两步就 if err,然而生活就是这样. 这是我理解的实现方式之...

  • go 异常处理

    error 接口声明如下: 创建error err:=errors.New("")//返回的是其返回的error类...

  • Go 异常处理

    1 error Go语言内置了一个简单的错误接口作为一种错误处理机制,接口定义如下: 它包含一个Error()方法...

  • Go 异常处理

    目录 panic和recover 作用 panic 能够改变程序的控制流,调用 panic 后会立刻停止执行当前函...

  • 异常处理

    异常处理 Go语言追求简洁优雅,所以,Go语言不支持传统的 try…catch…finally 这种异常,因为Go...

  • Golang 学习笔记八 错误异常

    一、错误异常 《快学 Go 语言》第 10 课 —— 错误与异常Go 语言的异常处理语法绝对是独树一帜,在我见过的...

  • golang的异常处理

    go语言是不支持异常的,go语言的设计者认为异常会被不成熟的程序员滥用,导致异常的处理过去复杂;go语言取消异常的...

  • (译)Go 语言中异常处理的艺术

    原文链接:The Art of Error Handling in Go Go 语言的异常处理与其他语言截然不同,...

网友评论

    本文标题:Go 异常处理

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