## 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 本章小結