defer 語句會將函數推遲到外層函數返回以後執行。數據結構
即defer後面的函數在defer語句所在的函數執行結束的時候會被調用函數
defer後面必須是函數調用語句,不能是其餘語句,不然編譯器會出錯ui
package main import "fmt" func main() { defer fmt.Println("world") fmt.Println("hello") }
輸出結果設計
hello world
Golang官方博客裏總結了defer的行爲規則,只有三條,咱們圍繞這三條進行說明。代理
官方給出一個例子,以下所示:指針
func a() { i := 0 defer fmt.Println(i) i++ return }
defer語句中的fmt.Println()參數i值在defer出現時就已經肯定下來,其實是拷貝了一份。後面對變量i的修改不會影響fmt.Println()函數的執行,仍然打印"0"。code
注意:對於指針類型參數,規則仍然適用,只不過延遲函數的參數是一個地址值,這種狀況下,defer後面的語句對變量的修改可能會影響延遲函數。blog
這個規則很好理解,定義defer相似於入棧操做,執行defer相似於出棧操做。資源
設計defer的初衷是簡化函數返回時資源清理的動做,資源每每有依賴順序,好比先申請A資源,再跟據A資源申請B資源,跟據B資源申請C資源,即申請順序是:A-->B-->C,釋放時每每又要反向進行。這就是把deffer設計成FIFO的緣由。編譯器
每申請到一個用完須要釋放的資源時,當即定義一個defer來釋放資源是個很好的習慣。
定義defer的函數,即主函數可能有返回值,返回值有沒有名字沒有關係,defer所做用的函數,即延遲函數可能會影響到返回值。
若要理解延遲函數是如何影響主函數返回值的,只要明白函數是如何返回的就足夠了。
有一個事實必需要了解,關鍵字return不是一個原子操做,實際上return只代理彙編指令ret,即將跳轉程序執行。好比語句return i
,實際上分兩步進行,即將i值存入棧中做爲返回值,而後執行跳轉,而defer的執行時機正是跳轉前,因此說defer執行時仍是有機會操做返回值的。
舉個實際的例子進行說明這個過程:
func deferFuncReturn() (result int) { i := 1 defer func() { result++ }() return i}
該函數的return語句能夠拆分紅下面兩行:
result = i return
而延遲函數的執行正是在return以前,即加入defer後的執行過程以下:
result = i result++ return
因此上面函數實際返回i++值。
關於主函數有不一樣的返回方式,但返回機制就如上機介紹所說,只要把return語句拆開均可以很好的理解,下面分別舉例說明
一個主函數擁有一個匿名的返回值,返回時使用字面值,好比返回"1"、"2"、"Hello"這樣的值,這種狀況下defer語句是沒法操做返回值的。
一個返回字面值的函數,以下所示:
func foo() int { var i int defer func() { i++ }() return 1}
上面的return語句,直接把1寫入棧中做爲返回值,延遲函數沒法操做該返回值,因此就沒法影響返回值。
一個主函數擁有一個匿名的返回值,返回使用本地或全局變量,這種狀況下defer語句能夠引用到返回值,但不會改變返回值。
一個返回本地變量的函數,以下所示:
func foo() int { var i int defer func() { i++ }() return i }
上面的函數,返回一個局部變量,同時defer函數也會操做這個局部變量。對於匿名返回值來講,能夠假定仍然有一個變量存儲返回值,假定返回值變量爲"anony",上面的返回語句能夠拆分紅如下過程:
anony = i i++ return
因爲i是整型,會將值拷貝給anony,因此defer語句中修改i值,對函數返回值不形成影響。
主函聲明語句中帶名字的返回值,會被初始化成一個局部變量,函數內部能夠像使用局部變量同樣使用該返回值。若是defer語句操做該返回值,可能會改變返回結果。
一個影響函返回值的例子:
func foo() (ret int) { defer func() { ret++ }() return 0}
上面的函數拆解出來,以下所示:
ret = 0 ret++ return
函數真正返回前,在defer中對返回值作了+1操做,因此函數最終返回1。
本節咱們嘗試瞭解一些defer的實現機制。
源碼包src/src/runtime/runtime2.go:_defer
定義了defer的數據結構:
type _defer struct { sp uintptr //函數棧指針 pc uintptr //程序計數器 fn *funcval //函數地址 link *_defer //指向自身結構的指針,用於連接多個defer}
咱們知道defer後面必定要接一個函數的,因此defer的數據結構跟通常函數相似,也有棧地址、程序計數器、函數地址等等。
與函數不一樣的一點是它含有一個指針,可用於指向另外一個defer,每一個goroutine數據結構中實際上也有一個defer指針,該指針指向一個defer的單鏈表,每次聲明一個defer時就將defer插入到單鏈表表頭,每次執行defer時就從單鏈表表頭取出一個defer執行。
下圖展現一個goroutine定義多個defer時的場景:
從上圖能夠看到,新聲明的defer老是添加到鏈表頭部。
函數返回前執行defer則是從鏈表首部依次取出執行,再也不贅述。
一個goroutine可能連續調用多個函數,defer添加過程跟上述流程一致,進入函數時添加defer,離開函數時取出defer,因此即使調用多個函數,也老是能保證defer是按FIFO方式執行的。
源碼包src/runtime/panic.go
定義了兩個方法分別用於建立defer和執行defer。
deferproc(): 在聲明defer處調用,其將defer函數存入goroutine的鏈表中;
deferreturn():在return指令,準確的講是在ret指令前調用,其將defer從goroutine鏈表中取出並執行。
能夠簡單這麼理解,在編譯在階段,聲明defer處插入了函數deferproc(),在函數return前插入了函數deferreturn()。
defer定義的延遲函數參數在defer語句出時就已經肯定下來了
defer定義順序與實際執行順序相反
return不是原子操做,執行過程是: 保存返回值(如有)-->執行defer(如有)-->執行ret跳轉
申請資源後當即使用defer關閉資源是好習慣