Go语言defer、panic、recover

2021/06/15

Tags: Go

摘要:理解Go语言defer、panic、recover。


defer

Go 语言的 defer 会在当前函数返回前执行传入的函数,它会经常被用于关闭文件描述符、关闭数据库连接以及解锁资源,总结一句话就是完成函数执行完的收尾工作。

func DeferDemo() {
    defer fmt.Println("this is defer println")
    fmt.Println("this is println")
}
// 输出
// this is println
// this is defer println

运行以上代码每次都是第二个println先输出,然后才是defer关键字修饰的println输出。

如果有多个defer,输出顺序又会如何?

func MultiDeferDemo() {
    for i := 0; i < 5; i++ {
        defer fmt.Println(" defer ", i)
    }
}
// 输出
// defer  4
// defer  3
// defer  2
// defer  1
// defer  0

每次最先输出的都是循环的最后一个println,可以得出:多个defer,运行顺序遵循LIFO规则。

defer的值传递问题。

基于defer的机制,可以用来统计函数的执行耗时。

func MethodElapsedTime() {
    start := time.Now()
    defer fmt.Println("elapsed ", time.Since(start))
    time.Sleep(time.Second * 2)
}
// 输出
// elapsed  169ns

上述代码的运行结果并不会和预期一致,调用 defer 关键字会立刻拷贝函数中引用的外部参数,所以 time.Since(start) 的结果不是在 main 函数退出之前计算的,而是在 defer 关键字调用时计算的。

如果想要达到预期结果,可以给defer传入匿名函数,这样调用defer关键字时,虽然也会进行参数拷贝,但是拷贝的是函数指针,并不是匿名函数的参数,这样就能拿到正确的参数。

func MethodElapsedTime1() {
    start := time.Now()
    defer func() {
        fmt.Println("elapsed ", time.Since(start))
    }()
    time.Sleep(time.Second * 2)
}
// 输出
// elapsed  2.001068253s

panic

When youpanicin Go, you’re freaking out, it’s not someone elses problem, it’s game over man.

从这句话可以得出,Panic是Go中的严重错误,会影响到程序的运行。

当panic异常发生时,程序会中断运行,并立即执行在该goroutine中被延迟的函数(defer 机制),随后,程序崩溃并输出日志信息。日志信息包括panic value和函数调用的堆栈跟踪信息。

func PanicDemo() {
    defer fmt.Println("defer println")
    go func() {
        defer fmt.Println("goroutine defer println")
        panic("")
    }()
    time.Sleep(time.Second * 2)
}

// 运行结果
// goroutine defer println
// panic: 
// 
//goroutine 7 [running]:
// archieyao.github.com/base/src/panic_demo.PanicDemo.func1()
//     /Users/archieyao/GoProjects/GoMod/base/src/panic_demo/panic_demo.go:16 +0x95
// created by archieyao.github.com/base/src/panic_demo.PanicDemo
//     /Users/archieyao/GoProjects/GoMod/base/src/panic_demo/panic_demo.go:14 +0x98

以上示例可以很好演示Panic的运行流程,在运行goroutine的匿名函数时,遇到了Panic,此时程序会先运行goroutine内的defer修饰的代码,然后输出崩溃日志,其中,最外层的 defer fmt.Println("defer println") 并未执行。

当panic不在goroutine中出现时,例如以下示例:

func PanicDemo1() {
    defer fmt.Println("defer println 1")
    panic("panic")
    defer fmt.Println("defer println 2")
}

此时程序运行的结果,会运行Panic前的defer,并且会运行Panic前的所有defer。

基于Panic的特性,当程序遇到Panic时,会运行Panic前的defer,如果在defer中构建匿名函数,在函数中再次Panic,就可以形成Panic嵌套。

func panicDemo2() {
    defer func() {
        defer func() {
            panic("panic 3")
        }()
        panic("panic 2")
    }()
    panic("panic 1")
}

// panic: panic 1
//    panic: panic 2
//    panic: panic 3 [recovered]
//    panic: panic 3

当多个Panic嵌套时,如果Panic都需要被执行的defer中,那每个Panic都会执行。

recover

recover一般都是用于恢复Panic,让程序崩溃后继续运行,类似于其他语言中的异常处理,当异常抛出后程序奔溃,但当捕获异常并处理后,程序不会崩溃。

func recoverDemo1() {
    catchErr()
    fmt.Println("after recover println")
}

func catchErr() {
    defer fmt.Println("defer println")
    defer func() {
        if err := recover(); err != nil {
            fmt.Println("recover")
        }
    }()
    panic("panic")
}

// 运行结果
// recover
// defer println
// after recover println

recover一般都是在defer中运行,常用写法如下:

func simpleRecover() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("recover")
        }
    }()
    panic("panic")

    // 注意 这行不会执行
    fmt.Println("bala")
}

一般在defer修饰的匿名函数中recover,并且加入判断,是否已经捕获到panic;值得注意的是,上述代码中,fmt.Println("bala") 这行是不会执行的,因为在simpleRecover()函数中,已经发生了Panic,程序已经中断了,会跳过后续的代码,然后去执行panic前的defer代码,然后在defer中恢复,此时虽然程序已经恢复到正常运行状态,但历史由于panic跳过的代码是无法回溯的。

recover() 的作用范围仅限于当前的所属 goroutine。发生 panic 时只会执行当前协程中的defer函数,其它协程里面的 defer 不会执行。

func simpleRecover() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("recover")
        }
    }()
    go func() {
        panic("panic")
    }()

    time.Sleep(time.Second*2)
    fmt.Println("bala")
}

以上代码中,由于panic是在新开启的goroutine中执行,recover是无法恢复这个goroutine中的panic,所以上述代码依然会崩溃。

func simpleRecover() {
    go func() {
        defer func() {
            if r := recover(); r != nil {
                fmt.Println("recover")
            }
        }()
        panic("panic")
    }()

    time.Sleep(time.Second*2)
    fmt.Println("bala")
}

如果把defer也放到新开启的goroutine中,就可以正常recover这个panic。此时代码也会正常往后运行,fmt.Println("bala") 这行也会输出,因为goroutine中panic已经恢复,不会跳过外层函数的代码。

参考:

https://draveness.me/golang/docs/part2-foundation/ch05-keyword/golang-defer/

https://segmentfault.com/a/1190000021141276