Go語(yǔ)言 更多關(guān)于延遲函數(shù)調(diào)用的知識(shí)點(diǎn)

2023-02-16 17:38 更新

延遲調(diào)用函數(shù)已經(jīng)在前面介紹過(guò)了。 限于當(dāng)時(shí)對(duì)Go的了解程度,很多延遲調(diào)用函數(shù)相關(guān)的細(xì)節(jié)和用例并沒(méi)有在之前的文章中提及。 這些細(xì)節(jié)和用例將在本文中列出。

很多有返回值的內(nèi)置函數(shù)是不能被延遲調(diào)用的

在Go中,自定義函數(shù)的調(diào)用的返回結(jié)果都可以被舍棄。 但是,大多數(shù)內(nèi)置函數(shù)(除了copyrecover)的調(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")
	}()
}

延遲調(diào)用的函數(shù)值的估值時(shí)刻

一個(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)生
}

延遲方法調(diào)用的屬主實(shí)參的估值時(shí)刻

前面的文章曾經(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)
}

延遲調(diào)用使得代碼更簡(jiǎn)潔和魯棒

一個(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)用可能會(huì)導(dǎo)致性能損失

延遲調(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)化。

延遲調(diào)用導(dǎo)致的暫時(shí)性內(nèi)存泄露

一個(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
}


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

掃描二維碼

下載編程獅App

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

編程獅公眾號(hào)