defer语句被用于预定对一个函数的调用。我们把这类被defer语句调用的函数称为延迟函数。注意,defer语句只能出现在函数或方法的内部。
一条defer语句总是以关键字defer开始。在defer的右边还必会有一条表达式语句,且它们之间要以空格" "分隔,如:
defer fmt.Println("The finishing touches.")

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

  defer语句的执行时机总是在直接包含它的那个函数把流程控制权交还给它的调用方的前一刻,无论defer语句出现在外围函数的函数体中的哪一个位置上。具体分为下面几种情况:
  当外围函数的函数体中的相应语句全部被正常执行完毕的时候,只有在该函数中的所有defer语句都被执行完毕之后该函数才会真正地结束执行。
  当外围函数的函数体中的return语句被执行的时候,只有在该函数中的所有defer语句都被执行完毕之后该函数才会真正地返回。
  当在外围函数中有运行时恐慌发生的时候,只有在该函数中的所有defer语句都被执行完毕之后该运行时恐慌才会真正地被扩散至该函数的调用方。
  总之,外围函数的执行的结束会由于其中defer语句的执行而被推迟。
  正因为defer语句有着这样的特性,所以它成为了执行释放资源或异常处理等收尾任务的首选。使用defer语句的优势有两个:一、收尾任务总会被执行,我们不会再因粗心大意而造成资源的浪费;二、我们可以把它们放到外围函数的函数体中的任何地方(一般是函数体开始处或紧跟在申请资源的语句的后面),而不是只能放在函数体的最后。这使得代码逻辑变得更加清晰,并且收尾任务是否被合理的指定也变得一目了然。
  在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语句被执行的顺序完全相反的。每当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

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

Go语言之defer的更多相关文章

  1. Go语言异常处理defer\panic\recover

    Go语言追求简洁优雅,所以,Go语言不支持传统的 try…catch…finally 这种异常,因为Go语言的设计者们认为,将异常与控制结构混在一起会很容易使得代码变得混乱.因为开发者很容易滥用异常, ...

  2. go语言的defer语句

    转: https://www.jianshu.com/p/5b0b36f398a2 ---------------------------------------------------------- ...

  3. Go语言之defer关键字

    类似于java中的finally, 在函数返回来执行, 它用来保证函数一定会作一些事情. package main import "fmt" func main() { defer ...

  4. go语言笔记——defer作用DB资源等free或实现调试

    defer 和追踪 关键字 defer 允许我们推迟到函数返回之前(或任意位置执行 return 语句之后)一刻才执行某个语句或函数(为什么要在返回之后才执行这些语句?因为 return 语句同样可以 ...

  5. go语言 defer 你不知道的秘密!

    go 语言的defer功能强大,对于资源管理非常方便,但是如果没用好,也会有陷阱哦.我们先来看几个例子. 例一: defer 是先进后出 这个很自然,后面的语句会依赖前面的资源,因此如果先前面的资源先 ...

  6. 【GoLang】转载:我为什么放弃Go语言,哈哈

    我为什么放弃Go语言 作者:庄晓立(Liigo) 日期:2014年3月 原创链接:http://blog.csdn.NET/liigo/article/details/23699459 转载请注明出处 ...

  7. 我为什么放弃Go语言

    有好几次,当我想起来的时候,总是会问自己:我为什么要放弃Go语言?这个决定是正确的吗?是明智和理性的吗?事实上我一直在认真思考这个问题. 开门见山地说,我当初放弃Go语言(golang),就是由于两个 ...

  8. go defer (go延迟函数)

    go defer (go延迟函数) Go语言的defer算是一个语言的新特性,至少对比当今主流编程语言如此.根据GO LANGUAGE SPEC的说法: A "defer" sta ...

  9. Golang入门教程(十三)延迟函数defer详解

    前言 大家都知道go语言的defer功能很强大,对于资源管理非常方便,但是如果没用好,也会有陷阱哦.Go 语言中延迟函数 defer 充当着 try...catch 的重任,使用起来也非常简便,然而在 ...

随机推荐

  1. flask开发restful api系列(8)-再谈项目结构

    上一章,我们讲到,怎么用蓝图建造一个好的项目,今天我们继续深入.上一章中,我们所有的接口都写在view.py中,如果几十个,还稍微好管理一点,假如上百个,上千个,怎么找?所有接口堆在一起就显得杂乱无章 ...

  2. $ npm install opencv ? 你试试?! 在windows环境下,使用node.js调用opencv攻略

    博主之前写过一篇文章<html5与EmguCV前后端实现——人脸识别篇>,叙述的是opencv和C#的故事.最近在公司服务器上更新了一套nodejs环境,早就听闻npm上有opencv模块 ...

  3. Android 字体颜色变化(点击)

    在开发的过程中,经常会遇到这样的场景,点击按钮,背景颜色发生变化:在drawable中,定义xxx.xml(selector) <selector xmlns:android="htt ...

  4. ural 1572 Yekaterinozavodsk Great Well

    #include <cstdio> #include <cstring> #include <cmath> #include <algorithm> # ...

  5. 读UNDO引发的db file sequential read

    SQL> select * from (select SESSION_ID, NAME, P1, P2, P3, WAIT_TIME, CURRENT_OBJ#, CURRENT_FILE#, ...

  6. ORACLE表空间

    在ORACLE数据库中,所有数据从逻辑结构上看都是存放在表空间当中,当然表空间下还有段.区.块等逻辑结构.从物理结构上看是放在数据文件中.一个表空间可由多个数据文件组成. 如下图所示,一个数据库由对应 ...

  7. 组播报文转发过程RPF

    单播报文的转发过程中,路由器并不关心组播源地址,只关心报文中的目的地址,通过目的地址决定向哪个接口转发.在组播中,报文是发送给一组接收者的,这些接收者用一个逻辑地址标识.路由器在接收到报文后,必须根据 ...

  8. Android笔记(二):从savedIndstanceState发散

    savedIndstanceState savedIndstanceState位于ActivityonCreate(Bundle savedInstanceState)方法的参数中.对这个参数的理解要 ...

  9. Python学习日志(六)

    字符串的方法及注释 字符串转义字符含义 字符串格式化符号含义 eg:'a'的ASCii码是97 格式化操作符辅助指令 format()字符串格式化方法 字符串的格式化是指统一字符串格式 format( ...

  10. IOS-UITableView开发常用各种方法总结

    实现列表有两种方式 方式一 继承UIViewController,实现UITableViewDataSource和UITableViewDelegate协议.声明UITableView. UserIn ...