Golang, 以 9 個簡短程式碼片段,弄懂 defer 的使用特點

林冠巨集發表於2019-03-24

作者:林冠巨集 / 指尖下的幽靈

掘金:juejin.im/user/587f0d…

部落格:www.cnblogs.com/linguanh/

GitHub : github.com/af913337456…

騰訊雲專欄: cloud.tencent.com/developer/u…

蟲洞區塊鏈專欄:www.chongdongshequ.com/article/153…


前序

deferGo語言中一個很重要的關鍵詞。本文主要以簡短的手法列舉出,它在不同的多種常見程式碼片段中,所體現出來的不一樣的效果。從筆試的角度來看,可以說是覆蓋了絕大部分題型。

此外,在本文之前,還有本人另一篇同樣使用例子的形式channel 資料型別做直觀講解的文章。

Golang, 以17個簡短程式碼片段,切底弄懂 channel 基礎

目錄

  • defer 的主要特點
  • 非引用傳參給defer呼叫的函式,且為非閉包函式情況
  • 傳遞引用給defer呼叫的函式,即使不使用閉包函式情況
  • 傳遞值給defer呼叫的函式,且非閉包函式情況
  • defer呼叫閉包函式,且內呼叫外部非傳參進來的變數的情況
  • defer呼叫閉包函式,若內部使用了傳參引數的值的情況
  • defer所呼叫的非閉包函式,引數如果是函式的情況
  • defer 不影響 return的值
  • 閉包函式對 defer 的影響

defer 的主要特點

  • 延遲呼叫
  • 所在的函式中,它在 returnpanic執行完畢 後被呼叫
  • 多個 defer,它們的被呼叫順序,為的形式。先進後出,先定義的後被呼叫
func Test_1(t *testing.T) {
	// defer 的呼叫順序。由下到上,為 棧的形式。先進後出
	defer0()   // ↑
	defer1()   // |
	defer2()   // |
	defer3()   // |
	//defer4() // |
	defer5()   // |
	defer6()   // |  
	defer7()   // |
	defer8()   // |  從下往上
}
複製程式碼

非引用傳參給defer呼叫的函式,且為非閉包函式,值不會受後面的改變影響

func defer0() {
	a := 3  // a 作為演示的引數
	defer fmt.Println(a) // 非引用傳參,非閉包函式中,a 的值 不會 受後面的改變影響
	a = a + 2
}
// 控制檯輸出 3
複製程式碼

傳遞引用給defer呼叫的函式,即使不使用閉包函式,值也受後面的改變影響

func myPrintln(point *int)  {
	fmt.Println(*point) // 輸出引用所指向的值
}
func defer1() {
	a := 3
	// &a 是 a 的引用。記憶體中的形式: 0x .... ---> 3
	defer myPrintln(&a) // 傳遞引用給函式,即使不使用閉包函式,值 會 受後面的改變影響
	a = a + 2
}
// 控制檯輸出 5
複製程式碼

傳遞值給defer呼叫的函式,且非閉包函式,值不會受後面的改變影響

func p(a int)  {
	fmt.Println(a)
}

func defer2() {
	a := 3
	defer p(a) // 傳遞值給函式,且非閉包函式,值 不會 受後面的改變影響
	a = a + 2
}
// 控制檯輸出: 3
複製程式碼

defer呼叫閉包函式,且內呼叫外部非傳參進來的變數,值受後面的改變影響

// 閉包函式內,事實是該值的引用
func defer3() {
	a := 3
	defer func() {
		fmt.Println(a) // 閉包函式內呼叫外部非傳參進來的變數,事實是該值的引用,值 會 受後面的改變影響
	}()
	a = a + 2  // 3 + 2 = 5
}
// 控制檯輸出: 5
複製程式碼
// defer4 會丟擲陣列越界錯誤。
func defer4() {
	a := []int{1,2,3}
	for i:=0;i<len(a);i++ {
		// 同 defer3 的閉包形式。因為 i 是外部變數,沒用通過傳參的形式呼叫。在閉包內,是引用。
		// 值 會 受 ++ 改變影響。導致最終 i 是3, a[3] 越界
		defer func() {
			fmt.Println(a[i])
		}()
	}
}
// 結果:陣列越界錯誤
複製程式碼

defer呼叫閉包函式,若內部使用了傳參引數的值。使用的是值

func defer5() {
	a := []int{1,2,3}
	for i:=0;i<len(a);i++ {
		// 閉包函式內部使用傳參引數的值。內部的值為傳參的值。同時引用是不同的
		defer func(index int) {
		        // index 有一個新地址指向它
			fmt.Println(a[index]) // index == i
		}(i)
		// 後進先出,3 2 1
	}
}
// 控制檯輸出: 
//     3
//     2
//     1
複製程式碼

defer所呼叫的非閉包函式,引數如果是函式,會按順序先執行(函式引數)

func calc(index string, a, b int) int {
	ret := a + b
	fmt.Println(index, a, b, ret)
	return ret
}
func defer6()  {
	a := 1
	b := 2
	// calc 充當了函式中的函式引數。即使在 defer 的函式中,它作為函式引數,定義的時候也會首先呼叫函式進行求值
	// 按照正常的順序,calc("10", a, b) 首先被呼叫求值。calc("122", a, b) 排第二被呼叫
	defer calc("1", a, calc("10", a, b))
	defer calc("12",a, calc("122", a, b))
}
// 控制檯輸出:
/**
10 1 2 3   // 第一個函式引數
122 1 2 3  // 第二個函式引數
12 1 3 4   // 倒數第一個 calc
1 1 3 4    // 倒數第二個 calc
*/
複製程式碼

defer 不影響 return的值

下面兩個例子的結論是:

  • 無論 defer 內部呼叫傳遞的是值還是引用。都不會改變 return 的返回結果。返回值的確定,比 defer 早
func defer7() int {
	a := 2
	defer func() {
		a = a + 2
	}()
	return a
}
// 控制檯輸出:2
複製程式碼
func add(i *int)  {
	*i = *i + 2
}

func defer8() int {
	a := 2
	defer add(&a)
	return a
}
// 控制檯輸出:2
複製程式碼

原理:

    例如:return a,此行程式碼經過編譯後,會被拆分為:
    1. 返回值 = a
    2. 呼叫 defer 函式
    3. return
複製程式碼

閉包函式對 defer 的影響

函式中,值傳遞引用傳遞它們的區別是比較簡單的,為基礎的 C 語言指標知識。

而對於為什麼 defer 修飾的閉包函式,如果函式內部不是使用傳參的引數時,它所能起到的引用修改作用。原理如下:

a := 2
func() {
    fmt.Println(a)
}()
a = a + 3
複製程式碼
// 記憶體
閉包外:
    1. a 例項化
    2. a地址 ---> 2
閉包內:
    1. a 地址被傳遞進來
    2. a地址 ---> 2
    3. a = a + 3
    4. 輸出 5
複製程式碼

相關文章