gopl-zh.github.com/ch5/ch5-08.md

8.4 KiB
Raw Blame History

5.8. Deferred函數

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

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

gopl.io/ch5/title1
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
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
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
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…
}

下面的代碼是fetch1.5節的改進版我們將http響應信息寫入本地文件而不是從標準輸出流輸出。我們通過path.Base提出url路徑的最後一段作爲文件名。

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