目錄html
defer
是Go語言提供的一種用於註冊延遲調用的機制:讓函數或語句能夠在當前函數執行完畢後(包括經過return正常結束或者panic致使的異常結束)執行。android
defer
語句一般用於一些成對操做的場景:打開鏈接/關閉鏈接;加鎖/釋放鎖;打開文件/關閉文件等。git
defer
在一些須要回收資源的場景很是有用,能夠很方便地在函數結束前作一些清理操做。在打開資源語句的下一行,直接一句defer就能夠在函數返回前關閉資源,可謂至關優雅。程序員
f, _ := os.Open("defer.txt") defer f.Close()
注意:以上代碼,忽略了err, 實際上應該先判斷是否出錯,若是出錯了,直接return. 接着再判斷f
是否爲空,若是f
爲空,就不能調用f.Close()
函數了,會直接panic的。github
程序員在編程的時候,常常須要打開一些資源,好比數據庫鏈接、文件、鎖等,這些資源須要在用完以後釋放掉,不然會形成內存泄漏。golang
可是程序員都是人,是人就會犯錯。所以常常有程序員忘記關閉這些資源。Golang直接在語言層面提供defer
關鍵字,在打開資源語句的下一行,就能夠直接用defer
語句來註冊函數結束後執行關閉資源的操做。由於這樣一顆「小小」的語法糖,程序員忘寫關閉資源語句的狀況就大大地減小了。shell
defer的使用其實很是簡單:數據庫
f,err := os.Open(filename) if err != nil { panic(err) } if f != nil { defer f.Close() }
在打開文件的語句附近,用defer語句關閉文件。這樣,在函數結束以前,會自動執行defer後面的語句來關閉文件。編程
固然,defer會有小小地延遲,對時間要求特別特別特別高的程序,能夠避免使用它,其餘通常忽略它帶來的延遲。安全
咱們先看一下官方對defer
的解釋:
Each time a 「defer」 statement executes, the function value and parameters to the call are evaluated as usual and saved anew but the actual function is not invoked. Instead, deferred functions are invoked immediately before the surrounding function returns, in the reverse order they were deferred. If a deferred function value evaluates to nil, execution panics when the function is invoked, not when the 「defer」 statement is executed.
翻譯一下:每次defer語句執行的時候,會把函數「壓棧」,函數參數會被拷貝下來;當外層函數(非代碼塊,如一個for循環)退出時,defer函數按照定義的逆序執行;若是defer執行的函數爲nil, 那麼會在最終調用函數的產生panic.
defer語句並不會立刻執行,而是會進入一個棧,函數return前,會按先進後出的順序執行。也說是說最早被定義的defer語句最後執行。先進後出的緣由是後面定義的函數可能會依賴前面的資源,天然要先執行;不然,若是前面先執行,那後面函數的依賴就沒有了。
在defer函數定義時,對外部變量的引用是有兩種方式的,分別是做爲函數參數和做爲閉包引用。做爲函數參數,則在defer定義時就把值傳遞給defer,並被cache起來;做爲閉包引用的話,則會在defer函數真正調用時根據整個上下文肯定當前的值。
defer後面的語句在執行的時候,函數調用的參數會被保存起來,也就是複製了一份。真正執行的時候,實際上用到的是這個複製的變量,所以若是此變量是一個「值」,那麼就和定義的時候是一致的。若是此變量是一個「引用」,那麼就可能和定義的時候不一致。
舉個例子:
func main() { var whatever [3]struct{} for i := range whatever { defer func() { fmt.Println(i) }() } }
執行結果:
2 2 2
defer後面跟的是一個閉包(後面會講到),i是「引用」類型的變量,最後i的值爲2, 所以最後打印了三個2.
有了上面的基礎,咱們來檢驗一下成果:
type number int func (n number) print() { fmt.Println(n) } func (n *number) pprint() { fmt.Println(*n) } func main() { var n number defer n.print() defer n.pprint() defer func() { n.print() }() defer func() { n.pprint() }() n = 3 }
執行結果是:
3 3 3 0
第四個defer語句是閉包,引用外部函數的n, 最終結果是3;
第三個defer語句同第四個;
第二個defer語句,n是引用,最終求值是3.
第一個defer語句,對n直接求值,開始的時候n=0, 因此最後是0;
有些狀況下,咱們會故意用到defer的先求值,再延遲調用的性質。想象這樣的場景:在一個函數裏,須要打開兩個文件進行合併操做,合併完後,在函數執行完後關閉打開的文件句柄。
func mergeFile() error { f, _ := os.Open("file1.txt") if f != nil { defer func(f io.Closer) { if err := f.Close(); err != nil { fmt.Printf("defer close file1.txt err %v\n", err) } }(f) } // …… f, _ = os.Open("file2.txt") if f != nil { defer func(f io.Closer) { if err := f.Close(); err != nil { fmt.Printf("defer close file2.txt err %v\n", err) } }(f) } return nil }
上面的代碼中就用到了defer的原理,defer函數定義的時候,參數就已經複製進去了,以後,真正執行close()函數的時候就恰好關閉的是正確的「文件」了,妙哉!能夠想像一下若是不這樣將f當成函數參數傳遞進去的話,最後兩個語句關閉的就是同一個文件了,都是最後一個打開的文件。
不過在調用close()函數的時候,要注意一點:先判斷調用主體是否爲空,不然會panic. 好比上面的代碼片斷裏,先判斷f
不爲空,纔會調用Close()
函數,這樣最安全。
若是defer像上面介紹地那樣簡單(其實也不簡單啦),這個世界就完美了。事情老是沒這麼簡單,defer用得很差,是會跳進不少坑的。
理解這些坑的關鍵是這條語句:
return xxx
上面這條語句通過編譯以後,變成了三條指令:
1. 返回值 = xxx 2. 調用defer函數 3. 空的return
1,3步纔是Return 語句真正的命令,第2步是defer定義的語句,這裏可能會操做返回值。
下面咱們來看兩個例子,試着將return語句和defer語句拆解到正確的順序。
第一個例子:
func f() (r int) { t := 5 defer func() { t = t + 5 }() return t }
拆解後:
func f() (r int) { t := 5 // 1. 賦值指令 r = t // 2. defer被插入到賦值與返回之間執行,這個例子中返回值r沒被修改過 func() { t = t + 5 } // 3. 空的return指令 return }
這裏第二步沒有操做返回值r, 所以,main函數中調用f()獲得5.
第二個例子:
func f() (r int) { defer func(r int) { r = r + 5 }(r) return 1 }
拆解後:
func f() (r int) { // 1. 賦值 r = 1 // 2. 這裏改的r是以前傳值傳進去的r,不會改變要返回的那個r值 func(r int) { r = r + 5 }(r) // 3. 空的return return }
所以,main函數中調用f()獲得1.
defer語句表達式的值在定義時就已經肯定了。下面展現三個函數:
func f1() { var err error defer fmt.Println(err) err = errors.New("defer error") return } func f2() { var err error defer func() { fmt.Println(err) }() err = errors.New("defer error") return } func f3() { var err error defer func(err error) { fmt.Println(err) }(err) err = errors.New("defer error") return } func main() { f1() f2() f3() }
運行結果:
<nil> defer error <nil>
第1,3個函數是由於做爲函數參數,定義的時候就會求值,定義的時候err變量的值都是nil, 因此最後打印的時候都是nil. 第2個函數的參數其實也是會在定義的時候求值,只不過,第2個例子中是一個閉包,它引用的變量err在執行的時候最終變成defer error
了。關於閉包在本文後面有介紹。
第3個函數的錯誤還比較容易犯,在生產環境中,很容易寫出這樣的錯誤代碼。最後defer語句沒有起到做用。
閉包是由函數及其相關引用環境組合而成的實體,即:
閉包=函數+引用環境
通常的函數都有函數名,可是匿名函數就沒有。匿名函數不能獨立存在,但能夠直接調用或者賦值於某個變量。匿名函數也被稱爲閉包,一個閉包繼承了函數聲明時的做用域。在Golang中,全部的匿名函數都是閉包。
有個不太恰當的例子,能夠把閉包當作是一個類,一個閉包函數調用就是實例化一個類。閉包在運行時能夠有多個實例,它會將同一個做用域裏的變量和常量捕獲下來,不管閉包在什麼地方被調用(實例化)時,均可以使用這些變量和常量。並且,閉包捕獲的變量和常量是引用傳遞,不是值傳遞。
舉個簡單的例子:
func main() { var a = Accumulator() fmt.Printf("%d\n", a(1)) fmt.Printf("%d\n", a(10)) fmt.Printf("%d\n", a(100)) fmt.Println("------------------------") var b = Accumulator() fmt.Printf("%d\n", b(1)) fmt.Printf("%d\n", b(10)) fmt.Printf("%d\n", b(100)) } func Accumulator() func(int) int { var x int return func(delta int) int { fmt.Printf("(%+v, %+v) - ", &x, x) x += delta return x } }
執行結果:
(0xc420014070, 0) - 1 (0xc420014070, 1) - 11 (0xc420014070, 11) - 111 ------------------------ (0xc4200140b8, 0) - 1 (0xc4200140b8, 1) - 11 (0xc4200140b8, 11) - 111
閉包引用了x變量,a,b可看做2個不一樣的實例,實例之間互不影響。實例內部,x變量是同一個地址,所以具備「累加效應」。
Golang被詬病比較多的就是它的error, 常常是各類error滿天飛。編程的時候老是會返回一個error, 留給調用者處理。若是是那種致命的錯誤,好比程序執行初始化的時候出問題,直接panic掉,免得上線運行後出更大的問題。
可是有些時候,咱們須要從異常中恢復。好比服務器程序遇到嚴重問題,產生了panic, 這時咱們至少能夠在程序崩潰前作一些「掃尾工做」,如關閉客戶端的鏈接,防止客戶端一直等待等等。
panic會停掉當前正在執行的程序,不僅是當前協程。在這以前,它會有序地執行完當前協程defer列表裏的語句,其它協程裏掛的defer語句不做保證。所以,咱們常常在defer裏掛一個recover語句,防止程序直接掛掉,這起到了try...catch
的效果。
注意,recover()函數只在defer的上下文中才有效(且只有經過在defer中用匿名函數調用纔有效),直接調用的話,只會返回nil
.
func main() { defer fmt.Println("defer main") var user = os.Getenv("USER_") go func() { defer func() { fmt.Println("defer caller") if err := recover(); err != nil { fmt.Println("recover success. err: ", err) } }() func() { defer func() { fmt.Println("defer here") }() if user == "" { panic("should set user env.") } // 此處不會執行 fmt.Println("after panic") }() }() time.Sleep(100) fmt.Println("end of main function") }
上面的panic最終會被recover捕獲到。這樣的處理方式在一個http server的主流程經常會被用到。一次偶然的請求可能會觸發某個bug, 這時用recover捕獲panic, 穩住主流程,不影響其餘請求。
程序員經過監控獲知這次panic的發生,按時間點定位到日誌相應位置,找到發生panic的緣由,三下五除二,修復上線。一看四周,你們都埋頭幹本身的事,簡直完美:偷偷修復了一個bug, 沒有發現!嘿嘿!
defer很是好用,通常狀況下不會有什麼問題。可是隻有深刻理解了defer的原理纔會避開它的溫柔陷阱。掌握了它的原理後,就會寫出易懂易維護的代碼。
【defer那些事】https://xiaozhou.net/something-about-defer-2014-05-25.html
【defer代碼案例】https://tiancaiamao.gitbooks.io/go-internals/content/zh/03.4.html
【閉包】https://www.kancloud.cn/liupengjie/go/576456
【閉包】http://blog.51cto.com/speakingbaicai/1703229
【閉包】https://blog.csdn.net/zhangzhebjut/article/details/25181151
【延遲】http://liyangliang.me/posts/2014/12/defer-in-golang/
【defer三條原則】https://leokongwq.github.io/2016/10/15/golang-defer.html
【defer代碼例子】http://www.javashuo.com/article/p-wtlokfou-bc.html
【defer panic】https://ieevee.com/tech/2017/11/23/go-panic.html
【defer panic】https://zhuanlan.zhihu.com/p/33743255
【defer asm】https://mp.weixin.qq.com/s?__biz=MzAxMTA4Njc0OQ==&mid=2651435875&idx=1&sn=73b1023e91a6570e21bf237cd960b3e1&chksm=80bb6d91b7cce487d8ee5f3674d6746e1c1ecd9c4a725ac65917df0bc3f0540afe723da7cd9c&scene=4&subscene=126&ascene=0&devicetype=android-27&version=27000336&nettype=WIFI&abtest_cookie=BQABAAgACgALABMAFAAFAJ2GHgAjlx4AWpkeAJuZHgCdmR4AAAA%3D&lang=zh_CN&pass_ticket=A7Puc6tZBvtO6nZuNYDlkpGr0ioRdWC8iFkDA7T6N9VXvMm380V3sUaRtsGlyQFt&wx_header=1