## defer 关键字
`defer`关键字将函数的执行推迟到周围的函数返回之前,这在文件输入和输出操作中被广泛使用,因为它使你不必记住何时关闭打开的文件。使用`defer`关键字,可以将关闭已打开文件的函数调用放在打开该文件的函数调用附近。在第 8 章“告诉 UNIX 系统该做什么”中你将学习`defer`在与文件相关的操作,在本节将介绍`defer`的两种不同用法。你还将在讨论`panic()`和`restore()`内置 Go 函数的部分以及与日志记录相关的部分中看到`defer`。
重要的是要记住,延迟函数在返回周围函数后以后进先出(LIFO)的顺序执行。简而言之,这意味着如果在同一个周围函数中先延迟函数`f1()`,然后再延迟函数`f2()`,然后再延迟函数`f3()`,则当周围函数即将返回时,将执行函数`f3()`,然后`f2()`,最后才是`f3()`。
由于对`defer`的定义尚不清楚,我认为你可以通过查看 Go 代码和`defer.go`程序的输出来更好地理解`defer`的用法,该代码将分为三部分。
第一部分的代码:
```Go
package main
import (
"fmt"
)
func d1() {
for i := 3; i > 0; i-- {
defer fmt.Print(i, " ")
}
}
```
除了 import 块,前面的 Go 代码还实现了一个名为`d1()`的函数。包含有`for`循环和`defer`语句,该语句将执行 3 次。
第二部分的代码:
```Go
func d2() {
for i := 3; i > 0; i-- {
defer func() {
fmt.Print(i, " ")
}()
}
fmt.Println()
}
```
在这部分代码中,你可以看到另一个函数的实现,该函数名为`d2()`。`d2()`函数还包含一个`for`循环和一个`defer`语句,它们还将执行 3 次。但是,这一次,将 defer 关键字应用于匿名函数,而不是单个`fmt.Print()`语句。此外,匿名函数不带任何参数。
最后一部分的代码:
```Go
func d3() {
for i := 3; i > 0; i-- {
defer func(n int) {
fmt.Print(n, " ")
}(i)
}
}
func main() {
d1()
d2()
fmt.Println()
d3()
fmt.Println()
}
```
除了调用`d1(),d2()和d3()`函数的`main()`函数之外,你还可以看到`d3()`函数的实现,该函数具有一个`for`循环,该循环使用匿名的`defer`关键字功能。但是,这一次,匿名函数需要一个名为`n`的整数参数。Go 代码告诉我们,`n`参数从`for`循环中使用的`i`变量中获取其值。
执行`defer.go`将会得的输出:
```shell
$ go run defer.go
1 2 3
0 0 0
1 2 3
```
你很可能会发现生成的输出非常复杂且难以理解,这证明如果代码不清晰,则操作和使用`defer`的结果可能会很棘手。
我们将从`d1()`函数生成的第一行输出`(1 2 3)`开始。`d1()`中`i`的值依次为`3、2和1`。 `d1()`中延迟的函数是 fmt.Print()语句;结果,当`d1()`函数将要返回时,你将以相反的顺序获得 for 循环的 i 变量的三个值,因为延迟的函数以 LIFO 顺序执行。
现在,让我解释一下`d2()`函数产生的第二行输出。我们得到三个零而不是`1 2 3`真是奇怪。但是,原因很简单。
在`for`循环结束之后,`i`的值为 0,因为正是`i`的值使`for`循环终止。但是,这里的棘手之处在于,因为没有参数,所以在 for 循环结束后评估了延迟的匿名函数,这意味着对于`i`值为 0,它会被评估三次,因此生成了输出。这种令人困惑的代码可能会导致在你的项目中创建烦人的错误,因此请避免使用它。
最后,我们将讨论输出的第三行,它是由`d3()`函数生成的。由于匿名函数的参数,每次推迟匿名函数时,它都会获取并因此使用`i`的当前值。结果,匿名函数的每次执行都需要处理不同的值,因此产生的输出也不同。
所以你应该清楚,使用`defer`的最佳方法是第三种方法,它出现在`d3()`函数中,这种方法以易于理解的方式在匿名函数中传递了所需的变量。
- 介绍
- 1 Go与操作系统
- 01.1 Go的历史
- 01.2 Go的未来
- 01.3 Go的优点
- 01.3.1 Go是完美的么
- 01.3.2 什么是预处理器
- 01.3.3 godoc
- 01.4 编译Go代码
- 2 理解 Go 的内部构造
- Go 编译器
- Go 的垃圾回收
- 三色算法
- 有关 Go 垃圾收集器操作的更多信息
- Maps, silces 与 Go 垃圾回收器
- Unsafe code
- 有关 unsafe 包
- 另一个 usafe 包的例子
- 从 Go 调用 C 代码
- 在同一文件用 Go 调用 C 代码
- 在单独的文件用 Go 调用 C 代码
- 从 C 调用 Go 代码
- Go 包
- C 代码
- defer 关键字
- 用 defer 打印日志
- Panic 和 Recover
- 单独使用 Panic 函数
- 两个好用的 UNIX 工具
- strace
- dtrace
- 配置 Go 开发环境
- go env 命令
- Go 汇编器
- 节点树
- 进一步了解 Go 构建
- 创建 WebAssembly 代码
- 对 Webassembly 的简单介绍
- 为什么 WebAssembly 很重要
- Go 与 WebAssembly
- 示例
- 使用创建好的 WebAssembly 代码
- Go 编码风格建议
- 练习和相关链接
- 本章小结
- 3 Go基本数据类型
- 03.1 Go循环
- 03.1.1 for循环
- 03.1.2 while循环
- 03.1.3 range关键字
- 03.1.4 for循环代码示例
- 03.3 Go切片
- 03.3.1 切片基本操作
- 03.3.2 切片的扩容
- 03.3.3 字节切片
- 03.3.4 copy()函数
- 03.3.5 多维切片
- 03.3.6 使用切片的代码示例
- 03.3.7 使用sort.Slice()排序
- 03.4 Go 映射(map)
- 03.4.1 Map值为nil的坑
- 03.4.2 何时该使用Map?
- 03.5 Go 常量
- 03.5.1 常量生成器:iota
- 03.6 Go 指针
- 03.7 时间与日期的处理技巧
- 03.7.1 解析时间
- 03.7.2 解析时间的代码示例
- 03.7.3 解析日期
- 03.7.4 解析日期的代码示例
- 03.7.5 格式化时间与日期
- 03.8 延伸阅读
- 03.9 练习
- 03.10 本章小结
- 9 并发-Goroutines,Channel和Pipeline
- 09.1 关于进程,线程和Go协程
- 09.1.1 Go调度器
- 09.1.2 并发与并行
- 09.2 Goroutines
- 09.2.1 创建一个Goroutine
- 09.2.2 创建多个Goroutine
- 09.3 优雅地结束goroutines
- 09.3.1 当Add()和Done()的数量不匹配时会发生什么?
- 09.4 Channel(通道)
- 09.4.1 通道的写入
- 09.4.2 从通道接收数据
- 09.4.3 通道作为函数参数传递
- 09.5 管道
- 09.6 延展阅读
- 09.7 练习
- 09.8 本章小结