這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
學習 Go 中的 defer 相關知識疑惑之處以及閉包的概念和用法。
Defer 的理解
按照官方的解釋,defer 後面的運算式會被放入一個列表中,在當前方法返回的時候,列表中的運算式就會被執行。一個方法中可以在一個或者多個地方使用defer運算式。文檔中一個例子如下:
func a() { i := 0 defer fmt.Println(i)// defer fmt.Println(2)// defer fmt.Println(3) i++ return}
列印出來的數值為 0 ,這就很詫異了,即使按照 defer 最後被執行,那 i 的數值也應該是 1 啊。仔細看了官方文檔原來原因:上面的這段代碼,defer 運算式中用到了i這個變數,i 在初始化之後的值為 0,接著程式執行到 defer 運算式這一行,運算式所用到的i的值就為 0 了,接著,運算式被放入 list,等待在 return 的時候被調用。所以,後面儘管有一個 i++ 語句,仍然不能改變運算式 fmt.Println(i) 的結果。接著我們在上述代碼中加上注釋的代碼,最後返回的結果分別是 3,2,0。這就驗證了一個:defer 運算式的調用順序是按照先進後出的方式。
在 defer 運算式中可以修改函數中的命名傳回值,如下代碼所示:
func c() (i int) { defer func() { i++ }() return 1}
傳回值變數名為 i,在 defer 運算式中可以修改這個變數的值。所以,雖然在 return 的時候給傳回值賦值為 1,後來 defer 修改了這個值,讓 i 自增了 1,所以,函數的傳回值是 2 而不是 1。這個可以解釋下面代碼中的疑問一。
匿名函數
一旦我們暫時還不想給一個函數取名字,可以用如下方式定義函數:
func(x, y int) int { return x + y }
相應的調用方式如下,注意最後一對小括弧,就是表示匿名函數的調用:
func(x, y int) int { return x + y } (3, 4)
下面這個例子就是展示了如何將匿名函數賦值給變數並對其進行調用:
package mainimport "fmt"func main() { f()}func f() { for i := 0; i < 4; i++ { // 匿名函數賦值給局部變數 g g := func(i int) { fmt.Printf("%d ", i) } // 利用變數調用匿名函數 g(i) fmt.Printf(" - g is of type %T and has value %v\n", g, g) }}
書上更進一步給我們一個執行個體如下,匿名函數跟 defer 結合的:
package mainimport "fmt"// 疑問一func f() (ret int) { defer func() { ret++ }() return 1}func main() { fmt.Println(f())}
最終返回的是 2,因為 ret ++ 是在執行 return 1 語句之後發生的。其實個人有點疑惑,return 返回的數字,就是給 ret 作為初始變數了嗎?或者說 return 返回的語句立馬就進入到 defer 定義的函數中了?
閉包與函數
函數只是一段可執行代碼,編譯後就“固化”了,每個函數在記憶體中只有一份執行個體,得到函數的進入點便可以執行函數了。在函數式程式設計語言中,函數是一等公民(First class value):第一類對象,我們不需要像命令式語言中那樣藉助函數指標,委託操作函數,函數可以作為另一個函數的參數或傳回值,可以賦給一個變數。函數可以嵌套定義,即在一個函數內部可以定義另一個函數,有了嵌套函數這種結構,便會產生閉包問題。而此時需要注意,閉包並不是函數。由於閉包存在內容相關的問題,也就是引用環境,不同的引用環境與同一個閉包能夠成不同的執行個體。在 OO 中,我們習慣將對象傳來傳去,而在函數式編程中,我們就需要轉換思想了,我們傳來傳去的是 函數。
應用閉包:將函數作為傳回值
如下兩個函數 Add2 和 Adder 都會返回簽名為 func(b int) int 的函數:
func Add2() (func(b int) int)func Adder(a int) (func(b int) int)
對應的執行個體如下所示:
package mainimport "fmt"func main() { // make an Add2 function, give it a name p2, and call it: p2 := Add2() fmt.Printf("Call Add2 for 3 gives: %v\n", p2(3)) // make a special Adder function, a gets value 3: TwoAdder := Adder(2) fmt.Printf("The result is: %v\n", TwoAdder(3))}// 將函數作為傳回值func Add2() func(b int) int { // 注意在這裡的結構 return func(b int) int { return b + 2 }}func Adder(a int) func(b int) int { return func(b int) int { return a + b }}
返回的數值如下:
Call Add2 for 3 gives: 5 The result is: 5
接著書上講了一個比較重要的例子,通過這個例子跟我們介紹一個概念:閉包函數儲存並積累其中的變數的值,不管外部函數退出與否,它都能夠繼續操作外部函數中的局部變數。
執行個體如下:
package mainimport "fmt"func main() { var f = Adder() fmt.Print(f(1), " - ") fmt.Print(f(20), " - ") fmt.Print(f(300))}// Adder() 函數中,函數作為傳回值func Adder() func(int) int { var x int // 閉包函數,對於連續的 return 用簡單的思維去理解 return func(delta int) int { x += delta return x }}
按照一般的思維,肯定會覺得傳回值:1-20-300。可以程式執行出來的結果卻為:1-21-321。這?我們按照思路理一下:三次調用函數 f 的過程中函數 Adder() 中變數 delta 的值分別為:1、20 和 300。對比輸出的結果,我們發現每一次 x 這個外部函數的局部變數並沒有在閉包中被清零!!!,也就是說變數 x 的數值在閉包中是被保留的。無論外部函數的狀態如何!!!
另外注意一點,閉包中使用到的變數可以在閉包函數體內也可以在閉包函數體外聲明。
計算函數執行時間
這一章中的最後一小節,講述了如何計算函數的執行時間,主要就是 time 包中的 Now() 和 Sub() 函數,對於函數最佳化,我們就可以通過函數的計算時間來進行比較:
start := time.Now()longCalculation()end := time.Now()delta := end.Sub(start)fmt.Printf("longCalculation took this amount of time: %s\n", delta)