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