go 學習筆記之解讀什麼是defer延遲函式

snowdreams1006發表於2019-10-18

Go 語言中有個 defer 關鍵字,常用於實現延遲函式來保證關鍵程式碼的最終執行,常言道: "未雨綢繆方可有備無患".

延遲函式就是這麼一種機制,無論程式是正常返回還是異常報錯,只要存在延遲函式都能保證這部分關鍵邏輯最終執行,所以用來做些資源清理等操作再合適不過了.

https://i.iter01.com/images/505126f828539e2ab2ef66322877ba94cfc1edc1b00a45625e13ef18afca9666.jpg
go-error-about-defer.jpg

出入成雙有始有終

日常開發程式設計中,有些操作總是成雙成對出現的,有開始就有結束,有開啟就要關閉,還有一些連續依賴關係等等.

一般來說,我們需要控制結束語句,在合適的位置和時機控制結束語句,手動保證整個程式有始有終,不遺漏清理收尾操作.

最常見的拷貝檔案操作大致流程如下:

  1. 開啟原始檔
srcFile, err := os.Open("fib.txt")
if err != nil {
	t.Error(err)
	return
}
  1. 建立目標檔案
dstFile, err := os.Create("fib.txt.bak")
if err != nil {
	t.Error(err)
	return
}
  1. 拷貝原始檔到目標檔案
io.Copy(dstFile, srcFile)
  1. 關閉目標檔案
dstFile.Close()
srcFile.Close()
  1. 關閉原始檔
srcFile.Close()

值得注意的是: 這種拷貝檔案的操作需要特別注意操作順序而且也不要忘記釋放資源,比如先開啟再關閉等等!

func TestCopyFileWithoutDefer(t *testing.T) {
	srcFile, err := os.Open("fib.txt")
	if err != nil {
		t.Error(err)
		return
	}

	dstFile, err := os.Create("fib.txt.bak")
	if err != nil {
		t.Error(err)
		return
	}

	io.Copy(dstFile, srcFile)

	dstFile.Close()
	srcFile.Close()
}

「雪之夢技術驛站」: 上述程式碼邏輯還是清晰簡單的,可能不會忘記釋放資源也能保證操作順序,但是如果邏輯程式碼比較複雜的情況,這時候就有一定的實現難度了!

可能是為了簡化類似程式碼的邏輯,Go 語言引入了 defer 關鍵字,創造了"延遲函式"的概念.

  • defer 的檔案拷貝
func TestCopyFileWithoutDefer(t *testing.T) {
	if srcFile, err := os.Open("fib.txt"); err != nil {
		t.Error(err)
		return
	} else {
		if dstFile,err := os.Create("fib.txt.bak");err != nil{
			t.Error(err)
			return
		}else{
			io.Copy(dstFile,srcFile)

			dstFile.Close()
			srcFile.Close()
		}
	}
}
  • defer 的檔案拷貝
func TestCopyFileWithDefer(t *testing.T) {
	if srcFile, err := os.Open("fib.txt"); err != nil {
		t.Error(err)
		return
	} else {
		defer srcFile.Close()

		if dstFile, err := os.Create("fib.txt.bak"); err != nil {
			t.Error(err)
			return
		} else {
			defer dstFile.Close()

			io.Copy(dstFile, srcFile)
		}
	}
}

上述示例程式碼簡單展示了 defer 關鍵字的基本使用方式,顯著的好處在於 Open/Close 是一對操作,不會因為寫到最後而忘記 Close 操作,而且連續依賴時也能正常保證延遲時機.

簡而言之,如果函式內部存在連續依賴關係,也就是說建立順序是 A->B->C 而銷燬順序是 C->B->A.這時候使用 defer 關鍵字最合適不過.

懶人福音延遲函式

官方文件相關表述見 Defer statements[1]

如果沒有 defer 延遲函式前,普通函式正常執行:

func TestFuncWithoutDefer(t *testing.T) {
	// 「雪之夢技術驛站」: 正常順序
	t.Log("「雪之夢技術驛站」: 正常順序")

	// 1 2
	t.Log(1)
	t.Log(2)
}

當新增 defer 關鍵字實現延遲後,原來的 1 被推遲到 2 後面而不是之前的 1 2 順序.

func TestFuncWithDefer(t *testing.T) {
	// 「雪之夢技術驛站」: 正常順序執行完畢後才執行 defer 程式碼
	t.Log(" 「雪之夢技術驛站」: 正常順序執行完畢後才執行 defer 程式碼")

	// 2 1
	defer t.Log(1)
	t.Log(2)
}

如果存在多個 defer 關鍵字,執行順序可想而知,越往後的越先執行,這樣才能保證按照依賴順序依次釋放資源.

func TestFuncWithMultipleDefer(t *testing.T) {
	// 「雪之夢技術驛站」: 猜測 defer 底層實現資料結構可能是棧,先進後出.
	t.Log(" 「雪之夢技術驛站」: 猜測 defer 底層實現資料結構可能是棧,先進後出.")

	// 3 2 1
	defer t.Log(1)
	defer t.Log(2)
	t.Log(3)
}

相信你已經明白了多個 defer 語句的執行順序,那就測試一下吧!

func TestFuncWithMultipleDeferOrder(t *testing.T) {
	// 「雪之夢技術驛站」: defer 底層實現資料結構類似於棧結構,依次倒敘執行多個 defer 語句
	t.Log(" 「雪之夢技術驛站」: defer 底層實現資料結構類似於棧結構,依次倒敘執行多個 defer 語句")

	// 2 3 1
	defer t.Log(1)
	t.Log(2)
	defer t.Log(3)
}

初步認識了 defer 延遲函式的使用情況後,我們再結合文件詳細解讀一下相關定義.

  • 英文原版文件

A "defer" statement invokes a function whose execution is deferred to the moment the surrounding function returns,either because the surrounding function executed a return statement,reached the end of its function body,or because the corresponding goroutine is panicking.

  • 中文翻譯文件

"defer"語句呼叫一個函式,該函式的執行被推遲到周圍函式返回的那一刻,這是因為周圍函式執行了一個return語句,到達了函式體的末尾,或者是因為相應的協程正在驚慌.

具體來說,延遲函式的執行時機大概分為三種情況:

周圍函式執行 return

because the surrounding function executed a return statement

return 後面的 t.Log(4) 語句自然是不會執行的,程式最終輸出結果為 3 2 1 說明了 defer 語句會在周圍函式執行 return 前依次逆序執行.

func funcWithMultipleDeferAndReturn() {
	defer fmt.Println(1)
	defer fmt.Println(2)
	fmt.Println(3)
	return
	fmt.Println(4)
}

func TestFuncWithMultipleDeferAndReturn(t *testing.T) {
	// 「雪之夢技術驛站」: defer 延遲函式會在包圍函式正常return之前逆序執行.
	t.Log(" 「雪之夢技術驛站」: defer 延遲函式會在包圍函式正常return之前逆序執行.")

	// 3 2 1
	funcWithMultipleDeferAndReturn()
}

周圍函式到達函式體

reached the end of its function body

周圍函式的函式體執行到結尾前逆序執行多個 defer 語句,即先輸出 3 後依次輸出 2 1. 最終函式的輸出結果是 3 2 1 ,也就說是沒有 return 宣告也能保證結束前執行完 defer 延遲函式.

func funcWithMultipleDeferAndEnd() {
	defer fmt.Println(1)
	defer fmt.Println(2)
	fmt.Println(3)
}

func TestFuncWithMultipleDeferAndEnd(t *testing.T) {
	// 「雪之夢技術驛站」: defer 延遲函式會在包圍函式到達函式體結尾之前逆序執行.
	t.Log(" 「雪之夢技術驛站」: defer 延遲函式會在包圍函式到達函式體結尾之前逆序執行.")

	// 3 2 1
	funcWithMultipleDeferAndEnd()
}

當前協程正驚慌失措

because the corresponding goroutine is panicking

周圍函式萬一發生 panic 時也會先執行前面已經定義好的 defer 語句,而 panic 後續程式碼因為沒有特殊處理,所以程式崩潰了也就無法執行.

函式的最終輸出結果是 3 2 1 panic ,如此看來 defer 延遲函式還是非常盡忠職守的,雖然心裡很慌但還是能保證老弱病殘先行撤退!

func funcWithMultipleDeferAndPanic() {
	defer fmt.Println(1)
	defer fmt.Println(2)
	fmt.Println(3)
	panic("panic")
	fmt.Println(4)
}

func TestFuncWithMultipleDeferAndPanic(t *testing.T) {
	// 「雪之夢技術驛站」: defer 延遲函式會在包圍函式panic驚慌失措之前逆序執行.
	t.Log(" 「雪之夢技術驛站」: defer 延遲函式會在包圍函式panic驚慌失措之前逆序執行.")

	// 3 2 1
	funcWithMultipleDeferAndPanic()
}

通過解讀 defer 延遲函式的定義以及相關示例,相信已經講清楚什麼是 defer 延遲函式了吧?

簡單地說,延遲函式就是一種未雨綢繆的規劃機制,幫助開發者程式設計程式時及時做好收尾善後工作,提前做好預案以準備隨時應對各種情況.

  • 當週圍函式正常執行到到達函式體結尾時,如果發現存在延遲函式自然會逆序執行延遲函式.
  • 當週圍函式正常執行遇到 return 語句準備返回給呼叫者時,存在延遲函式時也會執行,同樣滿足善後清理的需求.
  • 當週圍函式異常執行不小心 panic 驚慌失措時,程式存在延遲函式也不會忘記執行,提前做好預案發揮了作用.

所以不論是正常執行還是異常執行,提前做好預案總是沒錯的,基本上可以保證萬無一失,所以不妨考慮考慮 defer 延遲函式?

https://i.iter01.com/images/e958f079564dd8c5e0479ff3d52286cdeb2039b1059394933c5361d08d780f81.png
go-error-about-lovely.png

延遲函式應用場景

基本上成雙成對的操作都可以使用延遲函式,尤其是申請的資源前後存在依賴關係時更應該使用 defer 關鍵字來簡化處理邏輯.

下面舉兩個常見例子來說明延遲函式的應用場景.

  • Open/Close

檔案操作一般會涉及到開啟和開閉操作,尤其是檔案之間拷貝操作更是有著嚴格的順序,只需要按照申請資源的順序緊跟著defer 就可以滿足資源釋放操作.

func readFileWithDefer(filename string) ([]byte, error) {
	f, err := os.Open(filename)
	if err != nil {
		return nil, err
	}
	defer f.Close()
	return ioutil.ReadAll(f)
}
  • Lock/Unlock

鎖的申請和釋放是保證同步的一種重要機制,需要申請多個鎖資源時可能存在依賴關係,不妨嘗試一下延遲函式!

var mu sync.Mutex
var m = make(map[string]int)
func lookupWithDefer(key string) int {
	mu.Lock()
	defer mu.Unlock()
	return m[key]
}

總結以及下節預告

defer 延遲函式是保障關鍵邏輯正常執行的一種機制,如果存在多個延遲函式的話,一般會按照逆序的順序執行,類似於棧結構.

延遲函式的執行時機一般有三種情況:

  • 周圍函式遇到返回時
func funcWithMultipleDeferAndReturn() {
	defer fmt.Println(1)
	defer fmt.Println(2)
	fmt.Println(3)
	return
	fmt.Println(4)
}
  • 周圍函式函式體結尾處
func funcWithMultipleDeferAndEnd() {
	defer fmt.Println(1)
	defer fmt.Println(2)
	fmt.Println(3)
}
  • 當前協程驚慌失措中
func funcWithMultipleDeferAndPanic() {
	defer fmt.Println(1)
	defer fmt.Println(2)
	fmt.Println(3)
	panic("panic")
	fmt.Println(4)
}

本文主要介紹了什麼是 defer 延遲函式,通過解讀官方文件並配套相關程式碼認識了延遲函式,但是延遲函式中存在一些可能令人比較迷惑的地方.

https://i.iter01.com/images/213756d274b4ba1cfdd70406538c7a1e9bbfb27f6658afe70b323bd32a929c76.png
go-error-about-question.png

讀者不妨看一下下面的程式碼,將心裡的猜想和實際執行結果比較一下,我們下次再接著分享,感謝你的閱讀.

func deferFuncWithAnonymousReturnValue() int {
	var retVal int
	defer func() {
		retVal++
	}()
	return 0
}

func deferFuncWithNamedReturnValue() (retVal int) {
	defer func() {
		retVal++
	}()
	return 0
}

延伸閱讀參考文件

  • Defer_statements[2]
  • go 語言的 defer 語句[3]
  • Go defer 實現原理剖析[4]
  • go 語言 defer 你不知道的祕密![5]
  • Go 語言中 defer 的一些坑[6]
  • go defer (go 延遲函式)[7]

如果本文對你有所幫助,不用讚賞,點贊鼓勵一下就是最大的認可,順便也可以關注下微信公眾號「 雪之夢技術驛站 」喲!

https://i.iter01.com/images/7a0215b7e076bbdf0ec6e642dbc0c2537db05f2426b694d66154d8e9ea91cc16.jpg
雪之夢技術驛站.png

參考資料

[1]

Defer statements: https://golang.google.cn/ref/spec#Defer_statements

[2]

Defer_statements: https://golang.google.cn/ref/spec#Defer_statements

[3]

go語言的defer語句: https://www.jianshu.com/p/5b0b36f398a2

[4]

Go defer實現原理剖析: https://studygolang.com/articles/16067

[5]

go語言 defer 你不知道的祕密!: https://www.cnblogs.com/baizx/p/5024547.html

[6]

Go語言中defer的一些坑: https://www.jianshu.com/p/79c029c0bd58

[7]

go defer (go延遲函式): https://www.cnblogs.com/ysherlock/p/8150726.html

相關文章