Defer, Panic, Recover

1、简介

Go具有控制流程的常用机制:if,for,switch,goto。 它还有go语句在单独的goroutine中运行代码。 在这里,我想讨论一些不太常见的问题:Defer,Panic和Recover。

2、Defer

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来轻松解决,但如果函数更复杂,则问题可能不会那么容易被注意到并解决。 通过引入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语句允许我们考虑在打开它之后立即关闭每个文件,保证无论函数中的返回语句数量如何,文件都将被关闭。
Defer语句的行为是直截了当且可预测的。 有三个简单的规则:

2.1、在计算defer语句时,将计算延迟函数的参数。

在此示例中,在延迟Println调用时计算表达式“i”。 函数返回后,延迟调用将打印“0”。

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

2.2、在周围函数返回后,延迟函数调用以Last In First Out顺序执行。

如下函数将打印“3210”:

func b() {
    for i := 0; i < 4; i++ {
        defer fmt.Print(i)
    }
}

2.3、Defer函数可以读取并分配给返回函数的命名返回值。

在如下示例中,Defer函数在周围函数返回后递增返回值i。 因此,此函数最终返回 2:

func c() (i int) {
    defer func() { i++ }()
    return 1
}

这样便于修改函数的错误返回值;

3、Panic 和 Recover

Panic是一个内置函数,可以阻止普通的控制流。 当函数F调用panic时,F的执行停止,F中的任何Defer函数都正常执行,然后将F返回其调用者。 对于调用者,只会感受F为Panic。 该过程继续向上移动,直到当前goroutine中的所有函数都返回,此时 程序崩溃。 可以通过直接调用 panic 来启动 panic。 它们也可能由运行时错误引起,例如越界数组访问。
Recover是一个内置函数,可以重新控制 panic 的goroutine。 recover仅在defer函数内有用。 在正常执行期间,对recover的调用将返回nil并且没有其他效果。 如果当前goroutine处于 panic 状态,则对 recover 的调用将捕获 panic 并恢复正常执行。
这是一个演示panic和defer机制的示例程序:

package main

import "fmt"

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

func f() {
    defer func() {
        if r := recover(); r != nil {
            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)
}

函数g获取 int i,如果i大于3则发生panic,否则它用参数 i + 1 进行递归调用。 函数 f 推出一个调用recover并打印恢复值的函数(如果它是非零的)。 尝试在阅读之前描绘该程序的输出。
该程序将输出:

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中删除defer函数,则不会恢复panic并到达goroutine调用堆栈的顶部,从而终止程序。 此修改后的程序将输出:

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]

有关panic和recover的真实示例,请参阅Go标准库中的json包。 它使用一组递归函数对JSON编码的数据进行解码。 当遇到格式错误的JSON时,解析器调用panic将堆栈展开到顶级函数调用,该函数调用从panic中恢复并返回适当的错误值(请参阅 decode.go 中的decodeState类型的'error'和'unmarshal'方法)。
Go 库的原则是即使在包的内部使用了 panic,在它的对外接口(API)中也必须用 recover 处理成返回显式的错误。

4、其他

4.1、Defer类似Java中finally

使用过程中,defer类似Java中finally,即使panic(即java中 throw exception),依然能够执行

4.2、panic 只能在本 goroutine 处理

若尝试在main中recover goroutine中panic,将无法达到预期,程序仍然会结束

4.3、recover 只能在 defer 中有效

golang的要求,recover只能写在defer中

4.4、多使用recover除占用cpu外,不会影响服务正常

如果函数没有 panic,调用 recover 函数不会获取到任何信息,也不会影响当前进程。

5、参考文献

  1. Defer, Panic, and Recover
  2. Golang: 深入理解panic and recover
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,214评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,307评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,543评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,221评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,224评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,007评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,313评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,956评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,441评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,925评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,018评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,685评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,234评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,240评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,464评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,467评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,762评论 2 345

推荐阅读更多精彩内容