Go 語(yǔ)言 Deferred函數(shù)

2023-03-14 16:53 更新

原文鏈接:https://gopl-zh.github.io/ch5/ch5-08.html


5.8. Deferred函數(shù)

在findLinks的例子中,我們用http.Get的輸出作為html.Parse的輸入。只有url的內(nèi)容的確是HTML格式的,html.Parse才可以正常工作,但實(shí)際上,url指向的內(nèi)容很豐富,可能是圖片,純文本或是其他。將這些格式的內(nèi)容傳遞給html.parse,會(huì)產(chǎn)生不良后果。

下面的例子獲取HTML頁(yè)面并輸出頁(yè)面的標(biāo)題。title函數(shù)會(huì)檢查服務(wù)器返回的Content-Type字段,如果發(fā)現(xiàn)頁(yè)面不是HTML,將終止函數(shù)運(yùn)行,返回錯(cuò)誤。

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
}

下面展示了運(yùn)行效果:

$ 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
title1: https://golang.org/doc/gopher/frontpage.png has type image/png, not text/html

resp.Body.close調(diào)用了多次,這是為了確保title在所有執(zhí)行路徑下(即使函數(shù)運(yùn)行失敗)都關(guān)閉了網(wǎng)絡(luò)連接。隨著函數(shù)變得復(fù)雜,需要處理的錯(cuò)誤也變多,維護(hù)清理邏輯變得越來越困難。而Go語(yǔ)言獨(dú)有的defer機(jī)制可以讓事情變得簡(jiǎn)單。

你只需要在調(diào)用普通函數(shù)或方法前加上關(guān)鍵字defer,就完成了defer所需要的語(yǔ)法。當(dāng)執(zhí)行到該條語(yǔ)句時(shí),函數(shù)和參數(shù)表達(dá)式得到計(jì)算,但直到包含該defer語(yǔ)句的函數(shù)執(zhí)行完畢時(shí),defer后的函數(shù)才會(huì)被執(zhí)行,不論包含defer語(yǔ)句的函數(shù)是通過return正常結(jié)束,還是由于panic導(dǎo)致的異常結(jié)束。你可以在一個(gè)函數(shù)中執(zhí)行多條defer語(yǔ)句,它們的執(zhí)行順序與聲明順序相反。

defer語(yǔ)句經(jīng)常被用于處理成對(duì)的操作,如打開、關(guān)閉、連接、斷開連接、加鎖、釋放鎖。通過defer機(jī)制,不論函數(shù)邏輯多復(fù)雜,都能保證在任何執(zhí)行路徑下,資源被釋放。釋放資源的defer應(yīng)該直接跟在請(qǐng)求資源的語(yǔ)句后。在下面的代碼中,一條defer語(yǔ)句替代了之前的所有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
}

在處理其他資源時(shí),也可以采用defer機(jī)制,比如對(duì)文件的操作:

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]
}

調(diào)試復(fù)雜程序時(shí),defer機(jī)制也常被用于記錄何時(shí)進(jìn)入和退出函數(shù)。下例中的bigSlowOperation函數(shù),直接調(diào)用trace記錄函數(shù)的被調(diào)情況。bigSlowOperation被調(diào)時(shí),trace會(huì)返回一個(gè)函數(shù)值,該函數(shù)值會(huì)在bigSlowOperation退出時(shí)被調(diào)用。通過這種方式, 我們可以只通過一條語(yǔ)句控制函數(shù)的入口和所有的出口,甚至可以記錄函數(shù)的運(yùn)行時(shí)間,如例子中的start。需要注意一點(diǎn):不要忘記defer語(yǔ)句后的圓括號(hào),否則本該在進(jìn)入時(shí)執(zhí)行的操作會(huì)在退出時(shí)執(zhí)行,而本該在退出時(shí)執(zhí)行的,永遠(yuǎn)不會(huì)被執(zhí)行。

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被調(diào)用,程序都會(huì)記錄函數(shù)的進(jìn)入,退出,持續(xù)時(shí)間。(我們用time.Sleep模擬一個(gè)耗時(shí)的操作)

$ 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語(yǔ)句中的函數(shù)會(huì)在return語(yǔ)句更新返回值變量后再執(zhí)行,又因?yàn)樵诤瘮?shù)中定義的匿名函數(shù)可以訪問該函數(shù)包括返回值變量在內(nèi)的所有變量,所以,對(duì)匿名函數(shù)采用defer機(jī)制,可以使其觀察函數(shù)的返回值。

以double函數(shù)為例:

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

我們只需要首先命名double的返回值,再增加defer語(yǔ)句,我們就可以在double每次被調(diào)用時(shí),輸出參數(shù)以及返回值。

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"

可能double函數(shù)過于簡(jiǎn)單,看不出這個(gè)小技巧的作用,但對(duì)于有許多return語(yǔ)句的函數(shù)而言,這個(gè)技巧很有用。

被延遲執(zhí)行的匿名函數(shù)甚至可以修改函數(shù)返回給調(diào)用者的返回值:

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

在循環(huán)體中的defer語(yǔ)句需要特別注意,因?yàn)橹挥性诤瘮?shù)執(zhí)行完畢后,這些被延遲的函數(shù)才會(huì)執(zhí)行。下面的代碼會(huì)導(dǎo)致系統(tǒng)的文件描述符耗盡,因?yàn)樵谒形募急惶幚碇?,沒有文件會(huì)被關(guān)閉。

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…
}

一種解決方法是將循環(huán)體中的defer語(yǔ)句移至另外一個(gè)函數(shù)。在每次循環(huán)時(shí),調(diào)用這個(gè)函數(shù)。

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節(jié))的改進(jìn)版,我們將http響應(yīng)信息寫入本地文件而不是從標(biāo)準(zhǔn)輸出流輸出。我們通過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
}

對(duì)resp.Body.Close延遲調(diào)用我們已經(jīng)見過了,在此不做解釋。上例中,通過os.Create打開文件進(jìn)行寫入,在關(guān)閉文件時(shí),我們沒有對(duì)f.close采用defer機(jī)制,因?yàn)檫@會(huì)產(chǎn)生一些微妙的錯(cuò)誤。許多文件系統(tǒng),尤其是NFS,寫入文件時(shí)發(fā)生的錯(cuò)誤會(huì)被延遲到文件關(guān)閉時(shí)反饋。如果沒有檢查文件關(guān)閉時(shí)的反饋信息,可能會(huì)導(dǎo)致數(shù)據(jù)丟失,而我們還誤以為寫入操作成功。如果io.Copy和f.close都失敗了,我們傾向于將io.Copy的錯(cuò)誤信息反饋給調(diào)用者,因?yàn)樗扔趂.close發(fā)生,更有可能接近問題的本質(zhì)。

練習(xí)5.18:不修改fetch的行為,重寫fetch函數(shù),要求使用defer機(jī)制關(guān)閉文件。



以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)