Go的推迟、恐慌和恢复

一个defer语句推动一个函数调用到列表中。保存的调用列表在周围函数返回后执行。

go的推迟、恐慌和恢复

一个defer语句推动一个函数调用到列表中。保存的调用列表在周围函数返回后执行。

使用场景:Defer语句通常用于简化执行各种清理操作的函数

举个例子:

func CopyFile(dstName, srcName string) (written int64, err error) {
    src, err := os.Open(srcName)
    if err != nil {
        return
    }

    dst, err := os.Create(dstName)
    if err != nil {
        return
    }

    written, err = io.Copy(dst, src)
    dst.Close()
    src.Close()
    return
}
//这有效,但有一个错误。如果对 os.Create 的调用失败,该函数将返回而不关闭源文件,但是可以通过在第二个return语句之前调用src.Close 来解决,但是如果函数更加的复杂,那么这个问题将不会轻易的被注意到,更加优雅的做法是,打开文件之后,我们在第一个return语句之后,(因为一旦返回,证明打开失败,就不需要关闭文件了)执行defer src.Close()来延迟关闭文件,它将会在第二个os.Create()语句失败之后,第二个语句return 语句返回之后执行关闭。
//这也验证了那句话:一个defer语句推动一个函数调用的列表,保存的函数调用的列表,会在(周围)函数返回之后执行!!!这个周围二字要慢慢体会,很精辟!!

func CopyFile(dstName, srcName string) (written int64, err error) {
    src, err := os.Open(srcName)
    if err != nil {
        return
    }
    defer src.Close()

    dst, err := os.Create(dstName)
    if err != nil {
        return
    }
    defer dst.Close()

    return io.Copy(dst, src)
}


Defer 语句允许我们在打开每个文件后立即考虑关闭它,保证无论函数中有多少个 return 语句,文件都将被关闭。

defer 语句的行为是直接且可预测的。有三个简单的规则:

1.在计算 defer 语句时计算延迟函数的参数。

func a() {
    i := 0
    defer fmt.Println(i)
    i++
    return
}

2.延迟的函数调用在周围函数返回后以后进先出的顺序执行

func b() {
    for i := 0; i < 4; i++ {
        defer fmt.Print(i)
    }
}
此函数打印“3210”:

3.延迟函数可以读取并分配给返回函数的命名返回值。

func c() (i int) {
    defer func() { i++ }()
    return 1
}
//在此示例中,延迟函数 在周围函数返回后增加返回值 i 。因此,此函数返回 2

这样方便修改函数的错误返回值;我们很快就会看到一个例子。

Panic是一个内置函数,它停止普通的控制流并开始恐慌

也就是说:当一个函数F内部调用panic时,这个函数F的执行将停止,但是F中任何的延迟执行的函数将正常执行,然后F返回给它的调用者。对于调用者而言,F的行为就像是调用panic.这个过程继续向上堆栈,直到当前的goroutine中所有的函数返回。此时程序崩溃。恐慌可以通过直接调用恐慌来启动。他们可能是由运行时的错误引起:例如数组访问越界。

Recover是一个内置函数,可以重新控制恐慌的 goroutine

值得注意的是:Recover只在延迟调用的函数中有用,这个是为什么呢?

因为对于正常执行期间,调用recovery函数只会返回一个nil并且没有其他影响。但是如果当前的goroutine 处于恐慌时,调用recovery,recovery 将捕获给予goroutine 恐慌的值,并且恢复正常执行。

以下是一个演示恐慌和延迟机制的例子:

package main

import "fmt"

func main() {
    f()
    fmt.Println("Returned normally from f.")
}

func f() {
    defer func() {
        if r := recover(); r != nil {
        //r := recover()就是捕获引起恐慌的值,在根据这个值的空与否来进行进一步的操作。
            fmt.Println("Recovered in f", r)
        }
    }()
    fmt.Println("Calling g.")
    g(0)
    fmt.Println("Returned normally from g.")
}



func g(i int) {
    if i > 3 {
        fmt.Println("Panicking!")
        panic(fmt.Sprintf("%v", i))
    }
    defer fmt.Println("Defer in g", i)
    fmt.Println("Printing in g", i)
    g(i + 1)
}



猜测输出:
Calling g.
Printing in g", 0
Printing in g", 1
Printing in g", 2
Printing in g", 3
Panicking!
Returned normally from g.
Defer in g", 3
Defer in g", 2
Defer in g", 1
Defer in g", 0
Returned normally from f
Recovered in f 4

//判断错误是因为:延迟执行的打印语句
defer func() {
        if r := recover(); r != nil {
        //r := recover()就是捕获引起恐慌的值,在根据这个值的空与否来进行进一步的操作。
            fmt.Println("Recovered in f", r)
        }
    }()
    个人认为这些语句是在f 里面,所以要在main函数返回后执行所以要在Returned normally from f.这个语句之后,目前还未得到解决。





实际输出:

Calling g.
Printing in g 0
Printing in g 1
Printing in g 2
Printing in g 3
Panicking!
Defer in g 3
Defer in g 2
Defer in g 1
Defer in g 0
Recovered in f 4
Returned normally from f.

如果我们从 f 中删除延迟函数,恐慌不会恢复并到达 goroutine 调用堆栈的顶部,终止程序。这个修改后的程序为:
package main

import "fmt"

func main() {
    f()
    fmt.Println("Returned normally from f.")
}

func f() {
    
    fmt.Println("Calling g.")
    g(0)
    fmt.Println("Returned normally from g.")
}



func g(i int) {
    if i > 3 {
        fmt.Println("Panicking!")
        panic(fmt.Sprintf("%v", i))
    }
    defer fmt.Println("Defer in g", i)
    fmt.Println("Printing in g", i)
    g(i + 1)
}



这个修改后的程序将输出:

Calling g.
Printing in g 0
Printing in g 1
Printing in g 2
Printing in g 3
Panicking!
Defer in g 3
Defer in g 2
Defer in g 1
Defer in g 0
panic: 4
panic PC=0x2a9cd8
[stack trace omitted]

与我们捕获恐慌数据不同的是:我们看到,当我们不调用recover来捕获引起的数据时 ,程序就会奔溃,不会继续往下执行,并且会抛出引起恐慌的数据,和错误提示,让主函数停下来。

有关panicrecovery的真实示例,请参阅Go 标准库中的json 包。它使用一组递归函数对接口进行编码。如果在遍历值时发生错误,则调用 panic 将堆栈展开到顶级函数调用,该函数调用从 panic 中恢复并返回适当的错误值(参见 encodeState 类型的 ‘error’ 和 ‘marshal’ 方法在encode.go 中)。

Go 库中的约定是,即使包在内部使用 panic,其外部 API 仍会显示明确的错误返回值。

defer 的其他用途——释放互斥锁

mu.Lock()
defer mu.Unlock()

//个人觉得在并发编程那一章这个defer关键字释放互斥锁的功能还是很强大的!!!

defer 的其他用途——打印页脚

printHeader()
defer printFooter()

吐血总结:

defer 语句为控制流提供了一种不寻常而且强大的机制。

Tags: golang
Share: X (Twitter) Facebook LinkedIn