defer,panic 和 Recover

翻译自:https://blog.golang.org/defer-panic-and-recover

Golang的常用控制流程机制有:if,for,switch,goto,他还有go语句启动goroutine运行代码。在这里,我跟大家讨论一些不太常用的:defer,panic,recover

defer 语句会将一个函数添加到函数调用列表,等defer所在的函数返回时,会调用这个列表的所有函数(后进先出的方式)。延迟通常用于简化执行各种清理操作。

例如,让我们看一个打开两个文件并将其中一个文件的内容拷贝到另一个文件的函数:

func CopyFile(dstName, srcName string) (written uint64, 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
}

这虽然能正常运行,但是存在bug。如果os.Create失败,那么该函数会在没有关闭src文件就返回了。虽然这很容易通过在第二个return语句之前调用src.Close()来解决这个Bug,但是如果函数更复杂,则问题可能不会轻易被注意到并得到解决。通过使用defer语句,我们能够确保文件始终被关闭

func CopyFile(dstName, srcName string) (written uint64, 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语句被评估时,被defer语句修饰的函数的参数也会被评估。
    在此示例中,Println函数被defer修饰时就会计算表达式"i",函数返回后,延迟调用将会输出“0”
func a() {
    i := 0
    defer fmt.Prinln(i)
    i++
    return
}
  1. 在函数返回后,延迟调用函数按照后进先出的方式被调用.
    函数b将会输出3210
func b() {
    for i := 0; i < 4; i++ {
        defer fmt.Print(i)
    }
}
  1. 延迟函数可以读取和修改函数的命名返回值
    此示例中,defer修饰的函数会在周围函数放回后会修改返回值。因此,函数返回"2"
func c() (i int) {
    defer func() {i++} ()
    return i
}

panic是一个内置函数,他能打断当前普通的控制留,并开始panicing。当函数F调用panic时,F的执行就会停止。F函数中在panic之前用defer语句声明的函数都将会正常执行。然后F返回给他的调用者。对于调用F函数的函数来说,F函数就表现的像panic函数一样。这个表现会一直向上抛,直到当前goroutine的所有函数都返回了,此时程序崩溃。Panics可由直接调用panic函数引起,也可以由运行时错误引起。例如:数据越界访问等。

recover是一个内置函数,可以控制goroutin的panic。recover仅在defer语句修饰的函数内有用。在正常的函数执行中,调用recover会返回nil并且没有其他任何效果。如果当前的goroutine正在发生panicing,在defer修饰的函数中调用recover能够捕获panic的值并且恢复正常执行。

下面是一个演示panic和recover机制的示例程序

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时触发panics,否则,传递参数i+1来递归调用自己。函数f用defer声明了一个函数,函数中调用recover并打印recover到的值(在不为nil的情况下)。在程序运行之前尝试描述一下这个程序的输出。

这个程序的输出:

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将堆栈展开到顶级函数调用,该函数recover这panic并获得这个err值。

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

defer的其他用户还有很多,包括释放锁:

mu.Lock()
defer mu.Unlock()

输出HtmlFooter

printHeader()
defer printFooter()

总之defer语句(包括使用或不实用panic和recover)提供了强大的控制流程的机制。他能模拟

其他语言中的专用结构实现的许多功能。你试试看。

By Andrew Gerrand

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

推荐阅读更多精彩内容