延遲調(diào)用函數(shù)已經(jīng)在前面介紹過了。 限于當(dāng)時對Go的了解程度,很多延遲調(diào)用函數(shù)相關(guān)的細(xì)節(jié)和用例并沒有在之前的文章中提及。 這些細(xì)節(jié)和用例將在本文中列出。
在Go中,自定義函數(shù)的調(diào)用的返回結(jié)果都可以被舍棄。 但是,大多數(shù)內(nèi)置函數(shù)(除了copy
和recover
)的調(diào)用的返回結(jié)果都不可以舍棄(至少對于Go 1.19來說是如此)。 另一方面,我們已經(jīng)了解到延遲函數(shù)調(diào)用的所有返回結(jié)果必須都舍棄掉。 所以,很多內(nèi)置函數(shù)是不能被延遲調(diào)用的。
幸運的是,在實踐中,延遲調(diào)用內(nèi)置函數(shù)的需求很少見。 根據(jù)我的經(jīng)驗,只有append
函數(shù)有時可能會需要被延遲調(diào)用。 對于這種情形,我們可以延遲調(diào)用一個調(diào)用了append
函數(shù)的匿名函數(shù)來滿足這個需求。
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") // 編譯錯誤
defer func() {
_ = append(s[:1], "x", "y")
}()
}
一個被延遲調(diào)用的函數(shù)值是在其調(diào)用被推入延遲調(diào)用隊列之前被估值的。 例如,下面這個例子將輸出false
。
package main
import "fmt"
func main() {
var f = func () {
fmt.Println(false)
}
defer f()
f = func () {
fmt.Println(true)
}
}
一個被延遲調(diào)用的函數(shù)值可能是一個nil函數(shù)值。這種情形將導(dǎo)致一個恐慌。 對于這種情形,恐慌產(chǎn)生在此延遲調(diào)用被執(zhí)行而不是被推入延遲調(diào)用隊列的時候。 一個例子:
package main
import "fmt"
func main() {
defer fmt.Println("此行可以被執(zhí)行到")
var f func() // f == nil
defer f() // 將產(chǎn)生一個恐慌
fmt.Println("此行可以被執(zhí)行到")
f = func() {} // 此行不會阻止恐慌產(chǎn)生
}
前面的文章曾經(jīng)解釋過:一個延遲調(diào)用的實參也是在此調(diào)用被推入延遲調(diào)用隊列時估值的。 方法的屬主實參也不例外。比如,下面這個程序?qū)⒋蛴〕?code>1342。
package main
type T int
func (t T) M(n int) T {
print(n)
return t
}
func main() {
var t T
// t.M(1)是方法調(diào)用M(2)的屬主實參,因此它
// 將在M(2)調(diào)用被推入延遲調(diào)用隊列時被估值。
defer t.M(1).M(2)
t.M(3).M(4)
}
一個例子:
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()
}
上面哪個函數(shù)看上去更簡潔?顯然,第二個使用了延遲調(diào)用的函數(shù),雖然只是簡潔了些許。 另外第二個函數(shù)將導(dǎo)致更少的bug,因為第一個函數(shù)中含有太多的f.Close()
調(diào)用,從而有較高的幾率漏掉其中一個。
下面是另外一個延遲調(diào)用使得代碼更魯棒的例子。 如果doSomething
函數(shù)產(chǎn)生一個恐慌,則函數(shù)f2
在退出時將導(dǎo)致互斥鎖未解鎖。 所以函數(shù)f1
更魯棒。
var m sync.Mutex
func f1() {
m.Lock()
defer m.Unlock()
doSomething()
}
func f2() {
m.Lock()
doSomething()
m.Unlock()
}
延遲調(diào)用并非沒有缺點。對于早于1.13版本的官方標(biāo)準(zhǔn)編譯器來說,延遲調(diào)用將導(dǎo)致一些性能損失。 從Go官方工具鏈1.13版本開始,官方標(biāo)準(zhǔn)編譯器對一些常見的延遲調(diào)用場景做了很大的優(yōu)化。 因此,一般我們不必太在意延遲調(diào)用導(dǎo)致的性能損失。感謝Dan Scales實現(xiàn)了此優(yōu)化。
一個較大的延遲調(diào)用隊列可能會消耗很多內(nèi)存。 另外,某些資源可能因為某些調(diào)用被延遲的太久而未能被及時釋放。
比如,如果下面的例子中的函數(shù)需要處理大量的文件,則在此函數(shù)退出之前,將有大量的文件句柄得不到釋放。
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
}
對于這種情形,我們應(yīng)該使用一個匿名函數(shù)將需要及時執(zhí)行延遲的調(diào)用包裹起來。比如,上面的函數(shù)可以改進(jìn)為如下:
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() // 將在此循環(huán)步內(nèi)執(zhí)行
_, err = f.WriteString(file.content)
if err != nil {
return err
}
return f.Sync()
}(); err != nil {
return err
}
}
return nil
}
更多建議: