Go语言之defer

定义

defer语句被用于预定对一个函数的调用。我们把这类被defer语句调用的函数称为延迟函数。

注意,defer语句只能出现在函数或方法的内部。

一条defer语句总是以关键字defer开始。在defer的右边还必会有一条表达式语句,且它们之间要以空格" "分隔,如:

defer fmt.Println("The finishing touches.")

这里的表达式语句必须代表一个函数或方法的调用。注意,既然是表达式语句,那么一些调用表达式就是不被允许出现在这里的。比如,针对各种内建函数的那些调用表达式。因为它们不能被称为表达式语句。另外,在这个位置上出现的表达式语句是不能被圆括号括起来的。

defer语句的执行时机总是在直接包含它的那个函数把流程控制权交还给它的调用方的前一刻,无论defer语句出现在外围函数的函数体中的哪一个位置上

具体分为下面几种情况:

  • 当外围函数的函数体中的相应语句全部被正常执行完毕的时候,只有在该函数中的所有defer语句都被执行完毕之后该函数才会真正地结束执行。
  • 当外围函数的函数体中的return语句被执行的时候,只有在该函数中的所有defer语句都被执行完毕之后该函数才会真正地返回。
  • 当在外围函数中有运行时恐慌发生的时候,只有在该函数中的所有defer语句都被执行完毕之后该运行时恐慌才会真正地被扩散至该函数的调用方。

总之,外围函数的执行的结束会由于其中defer语句的执行而被推迟。

正因为defer语句有着这样的特性,所以它成为了执行释放资源或异常处理等收尾任务的首选。

defer优势

使用defer语句的优势有两个:

  1. 收尾任务总会被执行,我们不会再因粗心大意而造成资源的浪费;

  2. 我们可以把它们放到外围函数的函数体中的任何地方(一般是函数体开始处或紧跟在申请资源的语句的后面),而不是只能放在函数体的最后。这使得代码逻辑变得更加清晰,并且收尾任务是否被合理的指定也变得一目了然。

在defer语句中,我们调用的函数不但可以是已声明的命名函数,还可以是临时编写的匿名函数,就像这样:

defer func() { 
       fmt.Println("The finishing touches.")    
}()

注意,一个针对匿名函数的调用表达式是由一个函数字面量和一个代表了调用操作的一对圆括号组成的。

我们在这里选择匿名函数的好处是可以使该函数的收尾任务的内容更加直观。不过,我们也可以把比较通用的收尾任务单独放在一个命名函数中,然后再将其添加到需要它的defer语句中。无论在defer关键字右边的是命名函数还是匿名函数,我们都可以称之为延迟函数。因为它总是会被延迟到外围函数执行结束前一刻才被真正的调用。
每当defer语句被执行的时候,传递给延迟函数的参数都会以通常的方式被求值。如下例:

func begin(funcName string) string {
    fmt.Printf("Enter function %s.\n", funcName)    
    return funcName
}
func end(funcName string) string {
    fmt.Printf("Exit function %s.\n", funcName)    
    return funcName
}
func record() {
    defer end(begin("record"))    
    fmt.Println("In function record.")
}

outputs:

  Enter function record.
  In function record.
  Exit function record.

示例中,调用表达式begin("record")是作为record函数的参数出现的。它会在defer语句被执行的时候被求值。也就是说,在record函数的函数体被执行之处,begin函数就被调用了。然而,end函数却是在外围函数record执行结束的前一刻被调用的。

这样做除了可以避免参数值在延迟函数被真正调用之前再次发生改变而给该函数的执行造成影响之外,还是处于同一条defer语句可能会被多次执行的考虑。如下例:

func printNumbers() {
    for i := 0; i < 5; i++ {        
      defer fmt.Printf("%d ", i)    
    }
 }

outputs:

  4 3 2 1 0

在for语句的每次迭代的过程中都会执行一次其中的defer语句。在第一次迭代中,针对延迟函数的调用表达式最终会是fmt.Printf("%d", 0)。这是由于在defer语句被执行的时候,参数i先被求值为了0,随后这个值被代入到了原来的调用表达式中,并形成了最终的延迟函数调用表达式。显然,这时的调用表达式已经与原来的表达式有所不同了。所以,Go语言会把代入参数值之后的调用表达式另行存储。以此类推,后面几次迭代所产生的延迟函数调用表达式依次为:

fmt.Printf("%d ", 1)
fmt.Printf("%d ", 2)
fmt.Printf("%d ", 3)
fmt.Printf("%d ", 4)

defer语句执行顺序

对延迟函数调用表达式的求值顺序是与它们所在的defer语句被执行的顺序完全相反的。每当Go语言把已代入参数值的延迟函数调用表达式另行存储后,还会把它追加到一个专门为当前外围函数存储延迟函数调用表达式的列表中。而这个列表总是LIFO(Last In First Out,即后进先出)的。因此,这些延迟函数调用表达式的求值顺序会是:

fmt.Printf("%d ", 4)
fmt.Printf("%d ", 3)
fmt.Printf("%d ", 2)
fmt.Printf("%d ", 1)
fmt.Printf("%d ", 0)

例:

func appendNumbers(ints []int) (result []int) {
    result = append(ints, 1)    
    fmt.Println(result)    
    defer func() {  
          result = append(result, 2)    
    }()
    result = append(result, 3)    
    fmt.Println(result)    defer func() { 
           result = append(result, 4)    
    }()
    result = append(result, 5)    
    fmt.Println(result)    defer func() {
            result = append(result, 6)    
    }()    
    return result
 }

outputs:

  [0 1 3 5 6 4 2]

例:

func printNumbers() {
    for i := 0; i < 5; i++ {     
       defer func() {      
             fmt.Printf("%d ", i)     
       }()
    }
}

outputs:

  5 5 5 5 5

在defer语句被执行的时候传递给延迟函数的参数都会被求值,但是延迟函数调用表达式并不会在那时被求值。当我们把
fmt.Printf("%d ", i)
改为

defer func() {
            fmt.Printf("%d ", i)
}()

之后,虽然变量i依然是有效的,但是它所代表的值却已经完全不同了。在for语句的迭代过程中,其中defer语句被执行了5次。但是,由于我们并没有给延迟函数传递任何参数,所以Go语言运行时系统也就不需要对任何作为延迟函数的参数值的表达式进行求值(因为它们根本不存在)。在for语句被执行完毕的时候,共有5个延迟函数调用表达式被存储到了它们的专属列表中。注意,被存储在专属列表中的是5个相同的调用表达式:

defer func() {
            fmt.Printf("%d ", i)
}()

在printNumbers函数的执行即将结束的时候,那个专属列表中的延迟函数调用表达式就会被逆序的取出并被逐个的求值。然而,这时的变量i已经被修改为了5。因此,对5个相同的调用表达式的求值都会使标准输出上打印出5.
  如何修正这个问题呢?
  将defer语句修改为:

defer func(i int) {
            fmt.Printf("%d ", i)        
}(i)

我们虽然还是以匿名函数作为延迟函数,但是却为这个匿名函数添加了一个参数声明,并在代表调用操作的圆括号中加入了作为参数的变量i。这样,在defer语句被执行的时候,传递给延迟函数的这个参数i就会被求值。最终的延迟函数调用表达式也会类似于:

defer func(i int) {
            fmt.Printf("%d ", i)        
}(0)

又因为延迟函数声明中的参数i屏蔽了在for语句中声明的变量i,所以在延迟函数被执行的时候,其中那条打印语句中所使用的i值即为传递给延迟函数的那个参数值。

如果延迟函数是一个匿名函数,并且在外围函数的声明中存在命名的结果声明,那么在延迟函数中的代码是可以对命名结果的值进行访问和修改的。如下例:

func modify(n int) (number int) {
    fmt.Println(number)    
    defer func() { 
           number += n    
    }()
    number++    
    return
}
  • modify(2),结果为:3

虽然在延迟函数的声明中可以包含结果声明,但是其返回的结果值会在它被执行完毕时丢弃。因此,作为惯例,我们在编写延迟函数的声明的时候不会为其添加结果声明。另一方面,推荐以传参的方式提供延迟函数所需的外部值。如下例:

func modify(n int) (number int) {
    fmt.Println(number)   
     defer func(plus int) (result int) {
             result = n + plus        
             number += result        
             return    
    }(3)
    number++    
    return
}
  • modify(2),结果为:6

我们可以把想要传递给延迟函数的参数值依照规则放入到那个代表调用操作的圆括号中,就像调用普通函数那样。另一方面,虽然我们在延迟函数的函数体中返回了结果值,但是却不会产生任何效果。

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

推荐阅读更多精彩内容

  • 86.复合 Cases 共享相同代码块的多个switch 分支 分支可以合并, 写在分支后用逗号分开。如果任何模式...
    无沣阅读 1,361评论 1 5
  • 官网 中文版本 好的网站 Content-type: text/htmlBASH Section: User ...
    不排版阅读 4,380评论 0 5
  • Lua 5.1 参考手册 by Roberto Ierusalimschy, Luiz Henrique de F...
    苏黎九歌阅读 13,780评论 0 38
  • 丫头真是古灵精怪,无敌可爱。 一岁四个月的丫头已经懂事了。吃完早餐我叫老板买单,老板太忙没听见,宝宝拿着钱就走向老...
    菜鸟快跑阅读 293评论 0 4
  • 每件事,如果你能尽全力去完成,可以无愧的说一声:已尽人事。那一定会有意外的收获。比如论文这件事。 一直以来,对论文...
    小饼子的记事本阅读 542评论 0 1