先拋磚引玉defer的延遲調用:
defer特性:
1. 關鍵字 defer 用於注冊延遲調用。 2. 這些調用直到 return 前才被執。因此,可以用來做資源清理。 3. 多個defer語句,按先進后出的方式執行。 4. defer語句中的變量,在defer聲明時就決定了。
defer用途:
1. 關閉文件句柄 2. 鎖資源釋放 3. 數據庫連接釋放
好,廢話不多說,實例加深理解,我們先看看一段代碼
package main import "fmt" func main() { var users [5]struct{} for i := range users { defer fmt.Println(i) } }
輸出:4 3 2 1 0 ,defer 是先進后出,這個輸出沒啥好說的。
我們把上面的代碼改下:
defer 換上閉包:
package main import "fmt" func main() { var users [5]struct{} for i := range users { defer func() { fmt.Println(i) }() } }
輸出:4 4 4 4 4,很多人也包括我。預期的結果不是 4 3 2 1 0 嗎?官網對defer 閉包的使用大致是這個意思:
函數正常執行,由於閉包用到的變量 i 在執行的時候已經變成4,所以輸出全都是4。那么 如何正常輸出預期的 4 3 2 1 0 呢?
不用閉包,換成函數:
package main import "fmt" func main() { var users [5]struct{} for i := range users { defer Print(i) } } func Print(i int) { fmt.Println(i) }
函數正常延遲輸出:4 3 2 1 0。
我們再舉一個可能一不小心會犯錯的例子:
defer調用引用結構體函數
package main import "fmt" type Users struct { name string } func (t *Users) GetName() { // 注意這里是 * 傳地址 引用Users fmt.Println(t.name) } func main() { list := []Users{{"喬峰"}, {"慕容復"}, {"清風揚"}} for _, t := range list { defer t.GetName() } }
輸出:清風揚 清風揚 清風揚。
這個輸出並不會像我們預計的輸出:清風揚 慕容復 喬峰
可是按照前面的go defer函數中的使用說明,應該輸出清風揚 慕容復 喬峰才對啊?
那我們換一種方式來調用一下
package main import "fmt" type Users struct { name string } func (t *Users) GetName() { // 注意這里是 * 傳地址 引用Users fmt.Println(t.name) } func GetName(t Users) { // 定義一個函數,名稱自定義 t.GetName() // 調用結構體USers的方法GetName } func main() { list := []Users{{"喬峰"}, {"慕容復"}, {"清風揚"}} for _, t := range list { defer GetName(t) } }
輸出:清風揚 慕容復 喬峰。
這個時候輸出的就是所謂"預期"滴了
當然,如果你不想多寫一個函數,也很簡單,可以像下面這樣(改2處),同樣會輸出清風揚 慕容復 喬峰
package main import "fmt" type Users struct { name string } func (t *Users) GetName() { // 注意這里是 * 傳地址 引用Users fmt.Println(t.name) } func GetName(t Users) { // 定義一個函數,名稱自定義 t.GetName() // 調用結構體USers的方法GetName } func main() { list := []Users{{"喬峰"}, {"慕容復"}, {"清風揚"}} for _, t := range list { t2 := t // 定義新變量t2 t賦值給t2 defer t2.GetName() } }
輸出:清風揚 慕容復 喬峰。
通過以上例子
我們可以得出下面的結論:
defer后面的語句在執行的時候,函數調用的參數會被保存起來,但是不執行。也就是復制了一份。但是並沒有說struct這里的*指針如何處理,
通過這個例子可以看出go語言並沒有把這個明確寫出來的this指針(比如這里的* Users)當作參數來看待。到這里有滴朋友會說。看似多此一舉的聲明,
直接去掉指針調用 t *Users改成 t Users 不就行了?
package main import "fmt" type Users struct { name string } func (t Users) GetName() { // 注意這里是 * 傳地址 引用Users fmt.Println(t.name) } func main() { list := []Users{{"喬峰"}, {"慕容復"}, {"清風揚"}} for _, t := range list { defer t.GetName() } }
輸出:清風揚 慕容復 喬峰。這就回歸到上面的 defer 函數非引用調用的示例了。所以這里我們要注意defer后面的指針函數和普通函數的調用區別。很容易混淆出錯。
多個 defer 注冊,按 FILO 次序執行 ( 先進后出 )。哪怕函數或某個延遲調用發生錯誤,這些調用依舊會被執行,我們看看這一段
package main func users(i int) { defer println("北丐") defer println("南帝") defer func() { println("西毒") println(10 / i) // 異常未被捕獲,逐步往外傳遞,最終終止進程。 }() defer println("東邪") } func main() { users(0) println("武林排行榜,這里不會被輸出哦") }
輸出:
東邪 西毒 南帝 北丐 panic: runtime error: integer divide by zero goroutine 1 [running]: main.users.func1(0x0)
我們發現函數中異常,最后才捕獲輸出,但是一旦捕獲了異常,后面就不會再執行了,即終止了程序。
*延遲調用參數在求值或復制,指針或閉包會 "延遲" 讀取。
package main func test() { x, y := "喬峰", "慕容復" defer func(s string) { println("defer:", s, y) // y 閉包引用 輸出延遲和的值,即y+= 后的值=慕容復第二 }(x) // 匿名函數調用,傳送參數x 被復制,注意這里的x 是 喬峰,而不是下面的 x+= 后的值 x += "第一" y += "第二" println("x =", x, "y =", y) } func main() { test() }
輸出:
x = 喬峰第一 y = 慕容復第二 defer: 喬峰 慕容復第二
defer 與 return注意
package main import "fmt" func Users() (s string) { s = "喬峰" defer func() { fmt.Println("延遲執行后:"+s) }() return "清風揚" } func main() { Users() // 輸出:延遲執行后:清風揚 }
解釋:在有命名返回值的函數中(這里命名返回值為 s),執行 return "風清揚" 的時候實際上已經將s 的值重新賦值為 風清揚。
所以defer 匿名函數 輸出結果為 風清揚 而不是 喬峰。
在錯誤的位置使用 defer,來一段不嚴謹滴代碼:
package main import "net/http" func request() error { res, err := http.Get("http://www.google.com") // 不翻牆的情況下。是無法訪問滴 defer res.Body.Close() if err != nil { return err } // ..繼續業務code... return nil } func main() { request() }
輸出:
panic: runtime error: invalid memory address or nil pointer dereference [signal 0xc0000005 code=0x0 addr=0x40 pc=0x5e553e]
Why?因為在這里我們並沒有檢查我們的請求是否成功執行,當它失敗的時候,我們訪問了 Body 中的空變量 res ,所以會拋出異常。
怎么優化呢?
我們應該總是在一次成功的資源分配下面使用 defer ,簡單點說就是:當且僅當 http.Get 成功執行時才使用 defer.
package main import "net/http" func request() error { res, err := http.Get("http://www.google.com") if res != nil { defer res.Body.Close() } if err != nil { return err } // ..繼續業務code... return nil } func main() { request() }
這樣,當有錯誤的時候,err 會被返回,否則當整個函數返回的時候,會關閉 res.Body 。
解釋:在這里,同樣需要檢查 res 的值是否為 nil ,這是 http.Get 中的一個警告。
通常情況下,出錯的時候,返回的內容應為空並且錯誤會被返回,可當你獲得的是一個重定向 error 時, res 的值並不會為 nil ,
但其又會將錯誤返回。所以上面的代碼保證了無論如何 Body 都會被關閉。
另外我們再聊下關於文件的defer close。在這里,f.Close() 可能會返回一個錯誤,可這個錯誤會被我們忽略掉
我們看一段代碼:
package main import "os" func open() error { f, err := os.Open("result.json") // 確保文件名存在 if err != nil { return err } if f != nil { defer f.Close() } // ..code... return nil } func main() { open() }
表面上看似沒問題,其實f.Close可能關閉文件失敗,我們優化下:
package main import "os" func open() error { f, err := os.Open("result.json") if err != nil { return err } if f != nil { defer func() { if err := f.Close(); err != nil { return } }() } // ..code... return nil } func main() { open() }
如果有代碼潔癖優化強迫症滴,哈哈。這里我們還可以優化下,可以通過命名的返回變量來返回 defer 內的錯誤。 如下:
package main import "os" func open() (err error) { f, err := os.Open("result.json") if err != nil { return err } if f != nil { defer func() { if ferr := f.Close(); ferr != nil { err = ferr //這里 通過命名的返回變量ferr賦值給err 來返回 defer 內的錯誤 } }() } // ..code... return nil } func main() { open() }
最后一個容易忽視的問題:如果你嘗試使用相同的變量釋放不同的資源,那么這個操作可能無法正常執行
神馬意思?繼續看:
package main import ( "fmt" "os" ) func open() error { f, err := os.Open("result.json") if err != nil { return err } if f != nil { defer func() { if err := f.Close(); err != nil { fmt.Printf("延遲關閉文件result.json 錯誤 %v\n", err) } }() } // ..code... f, err = os.Open("result2.json") if err != nil { return err } if f != nil { defer func() { if err := f.Close(); err != nil { fmt.Printf("延遲關閉文件result2.json 錯誤 %v\n", err) } }() } return nil } func main() { open() }
輸出:
延遲關閉文件result.json 錯誤 close result2.json: file already closed
結論:當延遲函數執行時,只有最后一個變量會被用到,因此,f 變量 會成為最后那個資源 (result2.json)。
而且兩個 defer 都會將這個資源作為最后的資源來關閉,也就是優先關閉了result2.json后,再執行第一個defer Close result1.json的時候,
其實還是在關閉result2.json.這樣重復關閉同一個文件導致錯誤異常。腫么解決?很好辦?用io.Closer屬性
package main import ( "fmt" "io" "os" ) func open() error { f, err := os.Open("result.json") if err != nil { return err } if f != nil { defer func(f io.Closer) { // 注意修改滴地方 if err := f.Close(); err != nil { fmt.Printf("延遲關閉文件result.json 錯誤 %v\n", err) } }(f) // 注意修改滴地方 } // ..code... f, err = os.Open("result2.json") if err != nil { return err } if f != nil { defer func(f io.Closer) {// 注意修改滴地方 if err := f.Close(); err != nil { fmt.Printf("延遲關閉文件result2.json 錯誤 %v\n", err) } }(f)// 注意修改滴地方 } return nil } func main() { open() }
到此,關於Go中defer的使用總結到這里了,有更多的使用技巧或坑,歡迎諸位博友留言指正。。。。