Golang 中 defer 的五個坑 - 第三部分

來源:互聯網
上載者:User
這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。> 譯註:全文總共有四篇,本文為同系列文章的第三篇- [第一部分](https://studygolang.com/articles/12061)- [第二部分](https://studygolang.com/articles/12136)本文將側重於講解使用 defer 的一些技巧如果你對 defer 的基本操作還沒有清晰的認識,請先閱讀這篇 [文章](https://blog.learngoprogramming.com/golang-defer-simplified-77d3b2b817ff) (GCTT 出品的譯文 https://studygolang.com/articles/11907 )。## #1 —— 在延遲調用函數的外部使用 recover你總是應該在被延遲函數的內部調用 `recover()` ,當出現一個 *panic* 異常時,在 *defer* 外調用`recover()` 將無法捕獲這個異常,而且 `recover()` 的傳回值會是 *nil* 。例子```gofunc do() {recover()panic("error")}```輸出*recover* 並沒有成功捕獲異常。```panic: error```### 解決方案在延遲調用的函數內部使用 `recover()` 就能夠避免這個問題。```gofunc do() {defer func() {r := recover()fmt.Println("recovered:", r)}()panic("error")}```輸出```recovered: error```## #2 —— 在錯誤的位置使用 defer這個陷阱來自於這篇 [Go 的 50 個陰影](http://devs.cloudimmunity.com/gotchas-and-common-mistakes-in-go-golang/#anameclose_http_resp_bodyaclosinghttpresponsebody)。例子當 `http.Get` 失敗時會拋出異常。```gofunc do() error {res, err := http.Get("http://notexists")defer res.Body.Close()if err != nil {return err}// ..code...return nil}```輸出```panic: runtime error: invalid memory address or nil pointer dereference```### 發生了什嗎?因為在這裡我們並沒有檢查我們的請求是否成功執行,當它失敗的時候,我們訪問了 *Body* 中的空變數 *res* ,因此會拋出異常### 解決方案總是在一次成功的資源分派下面使用 *defer* ,對於這種情況來說意味著:若且唯若 *http.Get* 成功執行時才使用 *defer*```gofunc do() error {res, err := http.Get("http://notexists")if res != nil {defer res.Body.Close()}if err != nil {return err}// ..code...return nil}```在上述的代碼中,當有錯誤的時候,*err* 會被返回,否則當整個函數返回的時候,會關閉 *res.Body* 。### 旁註 1在這裡,你同樣需要檢查 *resp* 的值是否為 *nil* ,這是 *http.Get* 中的一個警告。通常情況下,出錯的時候,返回的內容應為空白並且錯誤會被返回,可當你獲得的是一個重新導向 *error* 時, *resp* 的值並不會為 *nil* ,但其又會將錯誤返回。上面的代碼保證了無論如何 *Body* 都會被關閉,如果你沒有打算使用其中的資料,那麼你還需要丟棄已經接收的資料。更多 [詳情](http://devs.cloudimmunity.com/gotchas-and-common-mistakes-in-go-golang/#anameclose_http_resp_bodyaclosinghttpresponsebody)。## #3 —— 不檢查錯誤簡單地將清理的邏輯委託給 *defer* 並不意味著資源的釋放就萬無一失了,你也可能會錯失有用的報錯資訊,讓一些潛在的問題石沉大海。### 反面教材在這裡,`f.Close()` 可能會返回一個錯誤,可這個錯誤會被我們忽略掉```gofunc do() error {f, err := os.Open("book.txt")if err != nil {return err}defer f.Close()// ..code...return nil}```### 改進一下最好還是檢查可能的錯誤而不是直接交給 *defer* 就完事,你可以把 *defer* 內的代碼寫成一個協助函數來簡化我們的代碼,這裡為了講解方便就沒有進行簡化。```gofunc do() error {f, err := os.Open("book.txt")if err != nil {return err}defer func() {if err := f.Close(); err != nil {// log etc}}()// ..code...return nil}```### 再改進一下你也可以通過命名的返回變數來返回 *defer* 內的錯誤。```gofunc do() (err error) {f, err := os.Open("book.txt")if err != nil {return err}defer func() {if ferr := f.Close(); ferr != nil {err = ferr}}()// ..code...return nil}```### 旁註 2你可以使用這個 [包](https://godoc.org/github.com/pkg/errors) 來整合多個不同的錯誤,這會非常必要因為 defer 中的 *f.Close* 可能會把之前的錯誤也覆蓋掉,將多個錯誤包裹在一起能夠將所有的錯誤資訊都寫入日誌,在診斷問題的時候能有更多的依據。你也可以使用這個 [包](https://github.com/kisielk/errcheck) 來查看你遺漏的本應該檢查錯誤的地方。## #4 —— 釋放相同的資源在第三小節中有一個小小的警告:如果你嘗試使用相同的變數釋放不同的資源,那麼這個操作可能無法正常執行。例子這段看似沒什麼問題的代碼嘗試第二次關閉相同的資源。第二個 *變數 f* 會被關閉兩次,因為 * f 變數* 會因第二個資源而改變它的值```gofunc do() error {f, err := os.Open("book.txt")if err != nil {return err}defer func() {if err := f.Close(); err != nil {// log etc}}()// ..code...f, err = os.Open("another-book.txt")if err != nil {return err}defer func() {if err := f.Close(); err != nil {// log etc}}()return nil}```輸出```closing resource #another-book.txtclosing resource #another-book.txt```### 發生了什麼正如我們所看到的,當延遲函數執行時,只有最後一個變數會被用到,因此,*f 變數* 會成為最後那個資源 (another-book.txt)。而且兩個 *defer* 都會將這個資源作為最後的資源來關閉### 解決方案```gofunc do() error {f, err := os.Open("book.txt")if err != nil {return err}defer func(f io.Closer) {if err := f.Close(); err != nil {// log etc}}(f)// ..code...f, err = os.Open("another-book.txt")if err != nil {return err}defer func(f io.Closer) {if err := f.Close(); err != nil {// log etc}}(f)return nil}```輸出```closing resource #another-book.txtclosing resource #book.txt```你也可以使用函數來避免上述問題的發生,參考我在 [這裡](https://blog.learngoprogramming.com/gotchas-of-defer-in-go-1-8d070894cb01#ac69) 講過的開閉模式。## #5 —— panic/recover 會取得並返回任意類型你可能認為你總是需要往 *panic* 中傳 *string* 或 *error* 類型的資料### 傳入 string```gofunc errorly() {defer func() {fmt.Println(recover())}()if badHappened {panic("error run run")}}```輸出```"error run run"```### 傳入 error```gofunc errorly() {defer func() {fmt.Println(recover())}()if badHappened {panic(errors.New("error run run")}}```輸出```"error run run"```### 傳入任意類型正如你所看到的 *panic* 可以接收 *string* 以及 [error類型](https://golang.org/pkg/builtin/#error) 。這意味著事實上你可以給 panic 傳 "任意類型" 的資料並能夠在 *defer* 中使用 *recover* 來擷取這個資料。```gotype myerror struct {}func (myerror) String() string {return "myerror there!"}func errorly() {defer func() {fmt.Println(recover())}()if badHappened {panic(myerror{})}}```### 為什麼可以這麼寫?這是因為 *panic* 的函數簽名顯示它可以接收 *interface{}* 類型,我們可以將它理解為 Go 中的 "任意類型"這是 *panic* 的簽名```gofunc panic(v interface{})```*recover* 的簽名```gofunc recover() interface{}```因此,基本上它會這樣運行```panic(value) -> recover() -> value```*recover* 會把傳入 *panic* 的值返回出來這一部分就先告一段落了,我們第四部分見!

via: https://blog.learngoprogramming.com/5-gotchas-of-defer-in-go-golang-part-iii-36a1ab3d6ef1

作者:Inanc Gumus 譯者:yujiahaol68 校對:polaris1119

本文由 GCTT 原創編譯,Go語言中文網 榮譽推出

本文由 GCTT 原創翻譯,Go語言中文網 首發。也想加入譯者行列,為開源做一些自己的貢獻嗎?歡迎加入 GCTT!
翻譯工作和譯文發表僅用於學習和交流目的,翻譯工作遵照 CC-BY-NC-SA 協議規定,如果我們的工作有侵犯到您的權益,請及時聯絡我們。
歡迎遵照 CC-BY-NC-SA 協議規定 轉載,敬請在本文中標註並保留原文/譯文連結和作者/譯者等資訊。
文章僅代表作者的知識和看法,如有不同觀點,請樓下排隊吐槽

996 次點擊  

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.