這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
一. 引子
書接上文,在發表了《對一段Go語言代碼輸出結果的簡要分析》一文之後,原問題提出者又有了新問題,這是一個典型Gopher學習Go的曆程,想必很多Gopher們,包括我自己都遇到過的。我們先來看看這段代碼(來自原問題提出者):
// https://play.golang.org/p/dOUFNj96EIQpackage mainimport "fmt"func main() { var i int = 1 defer fmt.Println("result =>",func() int { return i * 2 }()) i++}
這裡顯然有坑!初學者的常規邏輯一般是:defer是在main函數退出後執行,退出前i已經做了+1操作,值變成了2,這樣一來defer後的Println應該輸出:result => 4 才對!實際輸出結果呢?
result => 2
這怎麼可能?
實際上不光是defer這樣,即使用go關鍵字替換掉defer,輸出的結果也是一樣的:result => 2:
package mainimport ( "fmt" "time")func main() { var i int = 1 go fmt.Println("result =>",func() int { return i * 2 }()) i++ time.Sleep(3*time.Second)}
二. defer function分析
那麼究竟為什麼輸出的是2,而不是4呢?因為無論是go關鍵字還是defer關鍵字,在代碼執行到它們時,編譯器都要為它們後面的函數準備好函數調用的參數堆棧,要確定的參數值和參數類型大小。這樣一來就得去求值:對它們後面的函數的參數進行求值。
以本文第一個defer那個例子為例!我們需要為defer後面的函數進行參數求值:
defer fmt.Println("result =>",func() int { return i * 2 }())
此時defer後面的函數是Println,這裡Println有兩個輸入參數:”result =>”和func() int {return i * 2}(),前者就是一個字串常量值,而後者是一個函數調用,我們需要對該函數調用進行求值。而在此時,i依然為1,因此Println的第二個參數的求值結果為2,於是上面defer的調用就等價於:
defer fmt.Println("result =>",2)
因此,無論最終i的值變成了多少,defer最終的輸出都是:result => 2。go關鍵字後面的參數亦是如此。其實這個過程與為普通函數的調用做準備是一樣的,也要先對函數的參數進行求值,之後再進入函數體,只不過defer將進入函數執行的過程延遲到defer的調用方退出之前了。
搞清楚這個defer原理後,我們如果想在defer函數執行時輸出4,那麼使用一個閉包函數即可:
// https://play.golang.org/p/Eux7zpSr7O8package mainimport "fmt"func main() { var i int = 1 defer func() { fmt.Println("result =>", func() int { return i * 2 }()) }() i++}
這裡我們看到defer 後面是一個不帶任何參數的匿名函數,所謂的對參數求值也是無值可求。在main函數退出前,defer後面的匿名函數真正執行時i的值已經是2,因此閉包函數中的Println輸出4。
三. defer method分析
defer後面除了可以跟著普通函數調用外,還可以使用方法調用(method):
defer instance.Method(x,y)
這可能又會讓初學者有些迷惑,多數又是Method的receiver類型以及go自動對instance的Method調用解引用或求地址的問題,我們“趁熱打鐵”,再來基於上一篇文章《對一段Go語言代碼輸出結果的簡要分析》中的例子做些修改,看看將go關鍵字換成defer會是一種什麼情況:
//https://play.golang.org/p/T8CdRfEn2h4package mainimport ( "fmt")type field struct { name string}func (p *field) print() { fmt.Println(p.name)}func main() { data1 := []*field{{"one"}, {"two"}, {"three"}} for _, v := range data1 { defer v.print() } data2 := []field{{"four"}, {"five"}, {"six"}} for _, v := range data2 { defer v.print() }}
這段代碼運行起來輸出:
sixsixsixthreetwoone
有了《對一段Go語言代碼輸出結果的簡要分析》一文中的思路作為基礎,對上面這段代碼的分析也就不難了。沒錯,還是按照我上一篇的“等價轉換”思路去思考,將method轉換為function後,再分析。上面的代碼可以等價變換為下面代碼:
https://play.golang.org/p/a-vOSz4N3jbpackage mainimport ( "fmt")type field struct { name string}func print(p *field) { fmt.Println(p.name)}func main() { data1 := []*field{{"one"}, {"two"}, {"three"}} for _, v := range data1 { defer print(v) } data2 := []field{{"four"}, {"five"}, {"six"}} for _, v := range data2 { defer print(&v) }}
接下來,我們就利用defer的“參數即時求值”原理,對上面的代碼作分析:
data1的三次迭代:defer的參數求值完後,defer print(v)調用分別變成了:
- defer print(&field{“one”})
- defer print(&field{“two”})
- defer print(&field{“three”})
data2的三次迭代,defer的參數求值完後,defer print(v)調用分別變成了:
- defer print(&v)
- defer print(&v)
- defer print(&v)
於是在main退出前,defer函數按defer被調用的反向順序執行:
- print(&v)
- print(&v)
- print(&v)
- print(&field{“three”})
- print(&field{“two”})
- print(&field{“one”})
而此刻:v中儲存的值為field{“six”},於是前三次print均輸出”six”。
四. 小結
defer雖然帶來一些效能損耗,但defer的適當使用可以讓程式的邏輯結構變得更為簡潔。
《對一段Go語言代碼輸出結果的簡要分析》一文發出後,出乎意料地收到一些反饋,其實很多Go初學者希望能看到一些像這樣的入門,但又“較真”的,最好再涉及點底層實現的文章。以後有精力會多多關注這一點的。歡迎大家來本站繼續交流,從各位朋友提出的問題中,我也能收穫到靈感^0^。
著名雲主機服務廠商DigitalOcean發布最新的主機計劃,入門級Droplet配置升級為:1 core CPU、1G記憶體、25G高速SSD,價格5$/月。有使用DigitalOcean需求的朋友,可以開啟這個連結地址:https://m.do.co/c/bff6eed92687 開啟你的DO主機之路。
我的連絡方式:
微博:https://weibo.com/bigwhite20xx
公眾號:iamtonybai
部落格:tonybai.com
github: https://github.com/bigwhite
讚賞:
商務合作方式:撰稿、出書、培訓、線上課程、合夥創業、諮詢、廣告合作。