關於go 語言中的延遲執行函式
許多內建的帶有返回值的函式無法進行延遲呼叫
在go語言中,呼叫自定義函式的結果值可以全部不存在(丟棄)。但是,對於具有非空白返回結果列表的內建函式,他們的呼叫的結果不可以拋棄,copy和recover例外。 換句話說,延遲執行函式的結果必須被拋棄,所以許多內建函式無法被延遲。
幸運的是,在實踐中,很有偶需要延遲執行內建函式的地方。據我所知,只有append函式可能需要被延遲執行。這種情況下,我們可以把append包裝到一個延遲執行函式裡。
package main import "fmt" func main() { s := []string{"a", "b", "c", "d"} defer fmt.Println(s) // [a x y d] // defer append(s[:1], "x", "y") // error defer func() { _ = append(s[:1], "x", "y") }() }
延遲函式值的時刻
延遲函式呼叫中的被呼叫函式可以是零函式值。對於這種情況,恐慌將在準備延遲呼叫被推入當前goroutine的延遲呼叫棧之前發生。
package main import "fmt" func main() { defer fmt.Println("reachable") var f func() // f is nil by default defer f()// panic here // The following lines are dead code. fmt.Println("not reachable") f = func() {} }
在將延遲呼叫推入當前goroutine的延遲呼叫堆疊之前,還會計算延遲函式呼叫的引數
延遲呼叫使得程式碼清晰,並且bug少
import "os" func withoutDefers(filepath string, head, body []byte) error { f, err := os.Open(filepath) if err != nil { return err } _, err = f.Seek(16, 0) if err != nil { f.Close() return err } _, err = f.Write(head) if err != nil { f.Close() return err } _, err = f.Write(body) if err != nil { f.Close() return err } err = f.Sync() f.Close() return err } func withDefers(filepath string, head, body []byte) error { f, err := os.Open(filepath) if err != nil { return err } defer f.Close() _, err = f.Seek(16, 0) if err != nil { return err } _, err = f.Write(head) if err != nil { return err } _, err = f.Write(body) if err != nil { return err } return f.Sync() }
延遲函式呼叫導致的效能損失
使用延遲函式呼叫並不總是好的。到目前為止(Go 1.12),對於官方Go編譯器,延遲函式呼叫將在執行時導致一些效能損失.
例如,在以下示例中,CounterB和IncreaseB方法比CounterA和IncreaseA方法更有效。
import "sync" type T struct { mu sync.Mutex nint64 } func (t *T) CounterA() int64 { t.mu.Lock() defer t.mu.Unlock() return t.n } func (t *T) CounterB() (count int64) { t.mu.Lock() count = t.n t.mu.Unlock() return } func (t *T) IncreaseA() { t.mu.Lock() defer t.mu.Unlock() t.n++ } func (t *T) IncreaseB() { t.mu.Lock() t.n++ // this line will not panic for sure t.mu.Unlock() }
在B版本的函式中,我們應該保證Lock和Unlock呼叫之間的程式碼永遠不會出現恐慌。通常,建議在實踐中使用A版本功能。當我們真正關心所涉及功能的效能時,我們應該只採用B版本。
延遲函式呼叫導致的資源洩漏
非常大的延遲呼叫堆疊也可能消耗大量記憶體,並且未執行的延遲呼叫可能會阻止某些資源及時釋放。例如,如果在呼叫以下函式時需要處理許多檔案,則在函式退出之前將不會釋放大量檔案處理程式。
func writeManyFiles(files []File) error { for _, file := range files { f, err := os.Open(file.path) if err != nil { return err } defer f.Close() _, err = f.WriteString(file.content) if err != nil { return err } err = f.Sync() if err != nil { return err } } return nil }
對於這種情況,我們可以使用匿名函式來包含延遲呼叫,以便延遲函式呼叫將更早執行。例如,上述功能可以重寫和改進
func writeManyFiles(files []File) error { for _, file := range files { if err := func() error { f, err := os.Open(file.path) if err != nil { return err } defer f.Close() _, err = f.WriteString(file.content) if err != nil { return err } return f.Sync() }(); err != nil { return err } } return nil }