5.8. Deferred函數

在findLinks的例子中,我們用http.Get的輸出作爲html.Parse的輸入。隻有url的內容的確是HTML格式的,html.Parse才可以正常工作,但實際上,url指向的內容很豐富,可能是圖片,純文本或是其他。將這些格式的內容傳遞給html.parse,會産生不良後果。

下面的例子獲取HTML頁面併輸出頁面的標題。title函數會檢査服務器返迴的Content-Type字段,如果發現頁面不是HTML,將終止函數運行,返迴錯誤。

gopl.io/ch5/title1 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語句的函數執行完畢時,defer後的函數才會被執行,不論包含defer語句的函數是通過return正常結束,還是由於panic導致的異常結束。你可以在一個函數中執行多條defer語句,它們的執行順序與聲明順序相反。

defer語句經常被用於處理成對的操作,如打開、關閉、連接、斷開連接、加鎖、釋放鎖。通過defer機製,不論函數邏輯多複雜,都能保證在任何執行路徑下,資源被釋放。釋放資源的defer應該直接跟在請求資源的語句後。在下面的代碼中,一條defer語句替代了之前的所有resp.Body.Close

gopl.io/ch5/title2 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機製,比如對文件的操作:

io/ioutil 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章)

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語句後的圓括號,否則本該在進入時執行的操作會在退出時執行,而本該在退出時執行的,永遠不會被執行。

gopl.io/ch5/trace 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函數爲例:

func double(x int) int {
    return x + x
}

我們隻需要首先命名double的返迴值,再增加defer語句,我們就可以在double每次被調用時,輸出參數以及返迴值。

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語句的函數而言,這個技巧很有用。

被延遲執行的匿名函數甚至可以脩改函數返迴給調用者的返迴值:

func triple(x int) (result int) {
    defer func() { result += x }()
    return double(x)
}
fmt.Println(triple(4)) // "12"

在循環體中的defer語句需要特别註意,因爲隻有在函數執行完畢後,這些被延遲的函數才會執行。下面的代碼會導致繫統的文件描述符耗盡,因爲在所有文件都被處理之前,沒有文件會被關閉。

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語句移至另外一個函數。在每次循環時,調用這個函數。

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路徑的最後一段作爲文件名。

gopl.io/ch5/fetch 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機製關閉文件。