## 5.8. Deferred函數
在findLinks的例子中,我們用http.Get的輸出作為html.Parse的輸入。只有url的內容的確是HTML格式的,html.Parse才可以正常工作,但實際上,url指向的內容很豐富,可能是圖片,純文本或是其他。將這些格式的內容傳遞給html.parse,會產生不良后果。
下面的例子獲取HTML頁面并輸出頁面的標題。title函數會檢查服務器返回的Content-Type字段,如果發現頁面不是HTML,將終止函數運行,返回錯誤。
<u><i>gopl.io/ch5/title1</i></u>
```Go
func title(url string) error {
resp, err := http.Get(url)
if err != nil {
return err
}
// Check Content-Type is HTML (e.g., "text/html;charset=utf-8").
ct := resp.Header.Get("Content-Type")
if ct != "text/html" && !strings.HasPrefix(ct,"text/html;") {
resp.Body.Close()
return fmt.Errorf("%s has type %s, not text/html",url, ct)
}
doc, err := html.Parse(resp.Body)
resp.Body.Close()
if err != nil {
return fmt.Errorf("parsing %s as HTML: %v", url,err)
}
visitNode := func(n *html.Node) {
if n.Type == html.ElementNode && n.Data == "title"&&n.FirstChild != nil {
fmt.Println(n.FirstChild.Data)
}
}
forEachNode(doc, visitNode, nil)
return nil
}
```
下面展示了運行效果:
```
$ go build gopl.io/ch5/title1
$ ./title1 http://gopl.io
The Go Programming Language
$ ./title1 https://golang.org/doc/effective_go.html
Effective Go - The Go Programming Language
$ ./title1 https://golang.org/doc/gopher/frontpage.png
title: https://golang.org/doc/gopher/frontpage.png has type image/png, not text/html
```
resp.Body.close調用了多次,這是為了確保title在所有執行路徑下(即使函數運行失敗)都關閉了網絡連接。隨著函數變得復雜,需要處理的錯誤也變多,維護清理邏輯變得越來越困難。而Go語言獨有的defer機制可以讓事情變得簡單。
你只需要在調用普通函數或方法前加上關鍵字defer,就完成了defer所需要的語法。當執行到該條語句時,函數和參數表達式得到計算,但直到包含該defer語句的函數執行完畢時,defer后的函數才會被執行,不論包含defer語句的函數是通過return正常結束,還是由于panic導致的異常結束。你可以在一個函數中執行多條defer語句,它們的執行順序與聲明順序相反。
defer語句經常被用于處理成對的操作,如打開、關閉、連接、斷開連接、加鎖、釋放鎖。通過defer機制,不論函數邏輯多復雜,都能保證在任何執行路徑下,資源被釋放。釋放資源的defer應該直接跟在請求資源的語句后。在下面的代碼中,一條defer語句替代了之前的所有resp.Body.Close
<u><i>gopl.io/ch5/title2</i></u>
```Go
func title(url string) error {
resp, err := http.Get(url)
if err != nil {
return err
}
defer resp.Body.Close()
ct := resp.Header.Get("Content-Type")
if ct != "text/html" && !strings.HasPrefix(ct,"text/html;") {
return fmt.Errorf("%s has type %s, not text/html",url, ct)
}
doc, err := html.Parse(resp.Body)
if err != nil {
return fmt.Errorf("parsing %s as HTML: %v", url,err)
}
// ...print doc's title element…
return nil
}
```
在處理其他資源時,也可以采用defer機制,比如對文件的操作:
<u><i>io/ioutil</i></u>
```Go
package ioutil
func ReadFile(filename string) ([]byte, error) {
f, err := os.Open(filename)
if err != nil {
return nil, err
}
defer f.Close()
return ReadAll(f)
}
```
或是處理互斥鎖(9.2章)
```Go
var mu sync.Mutex
var m = make(map[string]int)
func lookup(key string) int {
mu.Lock()
defer mu.Unlock()
return m[key]
}
```
調試復雜程序時,defer機制也常被用于記錄何時進入和退出函數。下例中的bigSlowOperation函數,直接調用trace記錄函數的被調情況。bigSlowOperation被調時,trace會返回一個函數值,該函數值會在bigSlowOperation退出時被調用。通過這種方式, 我們可以只通過一條語句控制函數的入口和所有的出口,甚至可以記錄函數的運行時間,如例子中的start。需要注意一點:不要忘記defer語句后的圓括號,否則本該在進入時執行的操作會在退出時執行,而本該在退出時執行的,永遠不會被執行。
<u><i>gopl.io/ch5/trace</i></u>
```Go
func bigSlowOperation() {
defer trace("bigSlowOperation")() // don't forget the extra parentheses
// ...lots of work…
time.Sleep(10 * time.Second) // simulate slow operation by sleeping
}
func trace(msg string) func() {
start := time.Now()
log.Printf("enter %s", msg)
return func() {
log.Printf("exit %s (%s)", msg,time.Since(start))
}
}
```
每一次bigSlowOperation被調用,程序都會記錄函數的進入,退出,持續時間。(我們用time.Sleep模擬一個耗時的操作)
```
$ go build gopl.io/ch5/trace
$ ./trace
2015/11/18 09:53:26 enter bigSlowOperation
2015/11/18 09:53:36 exit bigSlowOperation (10.000589217s)
```
我們知道,defer語句中的函數會在return語句更新返回值變量后再執行,又因為在函數中定義的匿名函數可以訪問該函數包括返回值變量在內的所有變量,所以,對匿名函數采用defer機制,可以使其觀察函數的返回值。
以double函數為例:
```Go
func double(x int) int {
return x + x
}
```
我們只需要首先命名double的返回值,再增加defer語句,我們就可以在double每次被調用時,輸出參數以及返回值。
```Go
func double(x int) (result int) {
defer func() { fmt.Printf("double(%d) = %d\n", x,result) }()
return x + x
}
_ = double(4)
// Output:
// "double(4) = 8"
```
可能doulbe函數過于簡單,看不出這個小技巧的作用,但對于有許多return語句的函數而言,這個技巧很有用。
被延遲執行的匿名函數甚至可以修改函數返回給調用者的返回值:
```Go
func triple(x int) (result int) {
defer func() { result += x }()
return double(x)
}
fmt.Println(triple(4)) // "12"
```
在循環體中的defer語句需要特別注意,因為只有在函數執行完畢后,這些被延遲的函數才會執行。下面的代碼會導致系統的文件描述符耗盡,因為在所有文件都被處理之前,沒有文件會被關閉。
```Go
for _, filename := range filenames {
f, err := os.Open(filename)
if err != nil {
return err
}
defer f.Close() // NOTE: risky; could run out of file descriptors
// ...process f…
}
```
一種解決方法是將循環體中的defer語句移至另外一個函數。在每次循環時,調用這個函數。
```Go
for _, filename := range filenames {
if err := doFile(filename); err != nil {
return err
}
}
func doFile(filename string) error {
f, err := os.Open(filename)
if err != nil {
return err
}
defer f.Close()
// ...process f…
}
```
下面的代碼是fetch(1.5節)的改進版,我們將http響應信息寫入本地文件而不是從標準輸出流輸出。我們通過path.Base提出url路徑的最后一段作為文件名。
<u><i>gopl.io/ch5/fetch</i></u>
```Go
// Fetch downloads the URL and returns the
// name and length of the local file.
func fetch(url string) (filename string, n int64, err error) {
resp, err := http.Get(url)
if err != nil {
return "", 0, err
}
defer resp.Body.Close()
local := path.Base(resp.Request.URL.Path)
if local == "/" {
local = "index.html"
}
f, err := os.Create(local)
if err != nil {
return "", 0, err
}
n, err = io.Copy(f, resp.Body)
// Close file, but prefer error from Copy, if any.
if closeErr := f.Close(); err == nil {
err = closeErr
}
return local, n, err
}
```
對resp.Body.Close延遲調用我們已經見過了,在此不做解釋。上例中,通過os.Create打開文件進行寫入,在關閉文件時,我們沒有對f.close采用defer機制,因為這會產生一些微妙的錯誤。許多文件系統,尤其是NFS,寫入文件時發生的錯誤會被延遲到文件關閉時反饋。如果沒有檢查文件關閉時的反饋信息,可能會導致數據丟失,而我們還誤以為寫入操作成功。如果io.Copy和f.close都失敗了,我們傾向于將io.Copy的錯誤信息反饋給調用者,因為它先于f.close發生,更有可能接近問題的本質。
**練習5.18:**不修改fetch的行為,重寫fetch函數,要求使用defer機制關閉文件。
- 前言
- Go語言起源
- Go語言項目
- 本書的組織
- 更多的信息
- 致謝
- 入門
- Hello, World
- 命令行參數
- 查找重復的行
- GIF動畫
- 獲取URL
- 并發獲取多個URL
- Web服務
- 本章要點
- 程序結構
- 命名
- 聲明
- 變量
- 賦值
- 類型
- 包和文件
- 作用域
- 基礎數據類型
- 整型
- 浮點數
- 復數
- 布爾型
- 字符串
- 常量
- 復合數據類型
- 數組
- Slice
- Map
- 結構體
- JSON
- 文本和HTML模板
- 函數
- 函數聲明
- 遞歸
- 多返回值
- 錯誤
- 函數值
- 匿名函數
- 可變參數
- Deferred函數
- Panic異常
- Recover捕獲異常
- 方法
- 方法聲明
- 基于指針對象的方法
- 通過嵌入結構體來擴展類型
- 方法值和方法表達式
- 示例: Bit數組
- 封裝
- 接口
- 接口是合約
- 接口類型
- 實現接口的條件
- flag.Value接口
- 接口值
- sort.Interface接口
- http.Handler接口
- error接口
- 示例: 表達式求值
- 類型斷言
- 基于類型斷言識別錯誤類型
- 通過類型斷言查詢接口
- 類型分支
- 示例: 基于標記的XML解碼
- 補充幾點
- Goroutines和Channels
- Goroutines
- 示例: 并發的Clock服務
- 示例: 并發的Echo服務
- Channels
- 并發的循環
- 示例: 并發的Web爬蟲
- 基于select的多路復用
- 并發的退出
- 示例: 聊天服務
- 基于共享變量的并發
- 競爭條件
- sync.Mutex互斥鎖
- sync.RWMutex讀寫鎖
- 內存同步
- 競爭條件檢測
- 示例: 并發的非阻塞緩存
- Goroutines和線程
- 包和工具
- 包簡介
- 導入路徑
- 包聲明
- 導入聲明
- 包的匿名導入
- 包和命名
- 工具
- 測試
- go test
- 測試函數
- 測試覆蓋率
- 基準測試
- 剖析
- 示例函數
- 反射
- 為何需要反射?
- reflect.Type和reflect.Value
- Display遞歸打印
- 示例: 編碼S表達式
- 通過reflect.Value修改值
- 示例: 解碼S表達式
- 顯示一個類型的方法集
- 幾點忠告
- 底層編程
- unsafe.Sizeof, Alignof 和 Offsetof
- unsafe.Pointer
- 示例: 深度相等判斷
- 通過cgo調用C代碼
- 幾點忠告
- 附錄
- 附錄A:原文勘誤
- 附錄B:作者譯者
- 附錄C:譯文授權
- 附錄D:其它語言