自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

聽說,99% 的 Go 程序員都被 Defer 坑過

開發(fā) 后端
直接把我珍藏多年的代碼一把梭,憑借多年踩坑經(jīng)歷和寫 BUG 經(jīng)驗(yàn),我要站著把這個(gè)坑邁過去。

[[429635]]

先聲明:我被坑過。

之前寫 Go 專欄時(shí),寫過一篇文章:Go 專欄|錯(cuò)誤處理:defer,panic 和 recover。有小伙伴留言說:道理都懂,但還是不知道怎么用,而且還總出現(xiàn)莫名奇妙的問題。

出問題就對了,這個(gè)小東西壞的很,一不留神就出錯(cuò)。

所以,面對這種情況,我們今天就不講道理了。直接把我珍藏多年的代碼一把梭,憑借多年踩坑經(jīng)歷和寫 BUG 經(jīng)驗(yàn),我要站著把這個(gè)坑邁過去。

一、

先來一個(gè)簡單的例子熱熱身:

  1. package main 
  2.  
  3. import ( 
  4.     "fmt" 
  5.  
  6. func main() { 
  7.     defer func() { 
  8.         fmt.Println("first"
  9.     }() 
  10.  
  11.     defer func() { 
  12.         fmt.Println("second"
  13.     }() 
  14.  
  15.     fmt.Println("done"

輸出:

  1. done 
  2. second 
  3. first 

這個(gè)比較簡單,defer 語句的執(zhí)行順序是按調(diào)用 defer 語句的倒序執(zhí)行。

二、

看看這段代碼有什么問題?

  1. for _, filename := range filenames { 
  2.     f, err := os.Open(filename) 
  3.     if err != nil { 
  4.         return err 
  5.     } 
  6.     defer f.Close() 

這段代碼其實(shí)很危險(xiǎn),很可能會(huì)用盡所有文件描述符。因?yàn)?defer 語句不到函數(shù)的最后一刻是不會(huì)執(zhí)行的,也就是說文件始終得不到關(guān)閉。所以切記,一定不要在 for 循環(huán)中使用 defer 語句。

那怎么優(yōu)化呢?可以將循環(huán)體單獨(dú)寫一個(gè)函數(shù),這樣每次循環(huán)的時(shí)候都會(huì)調(diào)用關(guān)閉函數(shù)。

如下:

  1. for _, filename := range filenames { 
  2.     if err := doFile(filename); err != nil { 
  3.         return err 
  4.     } 
  5.  
  6. func doFile(filename string) error { 
  7.     f, err := os.Open(filename) 
  8.     if err != nil { 
  9.         return err 
  10.     } 
  11.     defer f.Close() 

三、

看看這三個(gè)函數(shù)的輸出結(jié)果是什么?

  1. package main 
  2.  
  3. import ( 
  4.     "fmt" 
  5.  
  6. func a() (r int) { 
  7.     defer func() { 
  8.         r++ 
  9.     }() 
  10.     return 0 
  11.  
  12. func b() (r int) { 
  13.     t := 5 
  14.     defer func() { 
  15.         t = t + 5 
  16.     }() 
  17.     return t 
  18.  
  19. func c() (r int) { 
  20.     defer func(r int) { 
  21.         r = r + 5 
  22.     }(r) 
  23.     return 1 
  24.  
  25. func main() { 
  26.     fmt.Println("a = ", a()) 
  27.     fmt.Println("b = ", b()) 
  28.     fmt.Println("c = ", c()) 

公布答案:

  1. a =  1 
  2. b =  5 
  3. c =  1 

你答對了嗎?

說實(shí)話剛開始看到這個(gè)結(jié)果時(shí),我是相當(dāng)費(fèi)解,完全不知道怎么回事。

但可以看到,這三個(gè)函數(shù)都有一個(gè)共同特點(diǎn),它們都有一個(gè)命名返回值,并且都在函數(shù)中引用了這個(gè)返回值。

引用的方式分兩種:分別是閉包和函數(shù)參數(shù)。

先看 a() 函數(shù):

閉包通過 r++ 修改了外部變量,返回值變成了 1。

相當(dāng)于:

  1. func aa() (r int) { 
  2.     r = 0 
  3.     // 在 return 之前,執(zhí)行 defer 函數(shù) 
  4.     func() { 
  5.         r++ 
  6.     }() 
  7.     return 

再看 b() 函數(shù):

閉包內(nèi)修改的只是局部變量 t,而外部變量 t 不受影響,所以還是返回 5。

相當(dāng)于:

  1. func bb() (r int) { 
  2.     t := 5 
  3.     // 賦值 
  4.     r = t 
  5.     // 在 return 之前,執(zhí)行 defer 函數(shù) 
  6.     // defer 函數(shù)沒有對返回值 r 進(jìn)行修改,只是修改了變量 t 
  7.     func() { 
  8.         t = t + 5 
  9.     }() 
  10.     return 

最后是 c 函數(shù):

參數(shù)傳遞是值拷貝,實(shí)參不受影響,所以還是返回 1。

相當(dāng)于:

  1. func cc() (r int) { 
  2.     // 賦值 
  3.     r = 1 
  4.     // 這里修改的 r 是函數(shù)形參的值 
  5.     // 值拷貝,不影響實(shí)參值 
  6.     func(r int) { 
  7.         r = r + 5 
  8.     }(r) 
  9.     return 

那么,為了避免寫出這么令人意外的代碼,最好在定義函數(shù)時(shí)就不要使用命名返回值。或者如果使用了,就不要在 defer 中引用。

再看下面兩個(gè)例子:

  1. func d() int { 
  2.     r := 0 
  3.     defer func() { 
  4.         r++ 
  5.     }() 
  6.     return r 
  7.  
  8. func e() int { 
  9.     r := 0 
  10.     defer func(i int) { 
  11.         i++ 
  12.     }(r) 
  13.     return 0 
  14. d =  0 
  15. e =  0 

返回值符合預(yù)期,再也不用絞盡腦汁猜了。

四、

defer 表達(dá)式的函數(shù)如果在 panic 后面,則這個(gè)函數(shù)無法被執(zhí)行。

  1. func main() { 
  2.     panic("a"
  3.     defer func() { 
  4.         fmt.Println("b"
  5.     }() 

輸出如下,b 沒有打印出來。

  1. panic: a 
  2.  
  3. goroutine 1 [running]: 
  4. main.main() 
  5.     xxx.go:87 +0x4ce 
  6. exit status 2 

而如果 defer 在前,則可以執(zhí)行。

  1. func main() { 
  2.     defer func() { 
  3.         fmt.Println("b"
  4.     }() 
  5.     panic("a"

輸出:

  1. panic: a 
  2.  
  3. goroutine 1 [running]: 
  4. main.main() 
  5.     xxx.go:90 +0x4e7 
  6. exit status 2 

五、

看看下面這段代碼的執(zhí)行順序:

  1. func G() { 
  2.     defer func() { 
  3.         fmt.Println("c"
  4.     }() 
  5.  
  6.     F() 
  7.     fmt.Println("繼續(xù)執(zhí)行"
  8.  
  9. func F() { 
  10.     defer func() { 
  11.         if err := recover(); err != nil { 
  12.             fmt.Println("捕獲異常:", err) 
  13.         } 
  14.         fmt.Println("b"
  15.     }() 
  16.     panic("a"
  17.  
  18. func main() { 
  19.     G() 

順序如下:

  1. 調(diào)用 G() 函數(shù);
  2. 調(diào)用 F() 函數(shù);
  3. F() 中遇到 panic,立刻終止,不執(zhí)行 panic 之后的代碼;
  4. 執(zhí)行 F() 中 defer 函數(shù),遇到 recover 捕獲錯(cuò)誤,繼續(xù)執(zhí)行 defer 中代碼,然后返回;
  5. 執(zhí)行 G() 函數(shù)后續(xù)代碼,最后執(zhí)行 G() 中 defer 函數(shù)。

輸出:

  1. 捕獲異常: a 
  2. 繼續(xù)執(zhí)行 

五、

看看下面這段代碼的執(zhí)行順序:

  1. func G() { 
  2.     defer func() { 
  3.         if err := recover(); err != nil { 
  4.             fmt.Println("捕獲異常:", err) 
  5.         } 
  6.         fmt.Println("c"
  7.     }() 
  8.  
  9.     F() 
  10.     fmt.Println("繼續(xù)執(zhí)行"
  11.  
  12. func F() { 
  13.     defer func() { 
  14.         fmt.Println("b"
  15.     }() 
  16.     panic("a"
  17.  
  18. func main() { 
  19.     G() 

順序如下:

  1. 調(diào)用 G() 函數(shù);
  2. 調(diào)用 F() 函數(shù);
  3. F() 中遇到 panic,立刻終止,不執(zhí)行 panic 之后的代碼;
  4. 執(zhí)行 F() 中 defer 函數(shù),由于沒有 recover,則將 panic 拋到 G() 中;
  5. G() 收到 panic 則不會(huì)執(zhí)行后續(xù)代碼,直接執(zhí)行 defer 函數(shù);
  6. defer 中捕獲 F() 拋出的異常 a,然后繼續(xù)執(zhí)行,最后退出。

輸出:

  1. 捕獲異常: a 

六、

看看下面這段代碼的執(zhí)行順序:

  1. func G() { 
  2.     defer func() { 
  3.         fmt.Println("c"
  4.     }() 
  5.  
  6.     F() 
  7.     fmt.Println("繼續(xù)執(zhí)行"
  8.  
  9. func F() { 
  10.     defer func() { 
  11.         fmt.Println("b"
  12.     }() 
  13.     panic("a"
  14.  
  15. func main() { 
  16.     G() 

順序如下:

  1. 調(diào)用 G() 函數(shù);
  2. 調(diào)用 F() 函數(shù);
  3. F() 中遇到 panic,立刻終止,不執(zhí)行 panic 之后的代碼;
  4. 執(zhí)行 F() 中 defer 函數(shù),由于沒有 recover,則將 panic 拋到 G() 中;
  5. G() 收到 panic 則不會(huì)執(zhí)行后續(xù)代碼,直接執(zhí)行 defer 函數(shù);
  6. 由于沒有 recover,直接拋出 F() 拋過來的異常 a,然后退出。

輸出:

  1. panic: a 
  2.  
  3. goroutine 1 [running]: 
  4. main.F() 
  5.     xxx.go:90 +0x5b 
  6. main.G() 
  7.     xxx.go:82 +0x48 
  8. main.main() 
  9.     xxx.go:107 +0x4a5 
  10. exit status 2 

七、

看看下面這段代碼的執(zhí)行順序:

  1. func G() { 
  2.     defer func() { 
  3.         // goroutine 外進(jìn)行 recover 
  4.         if err := recover(); err != nil { 
  5.             fmt.Println("捕獲異常:", err) 
  6.         } 
  7.         fmt.Println("c"
  8.     }() 
  9.  
  10.     // 創(chuàng)建 goroutine 調(diào)用 F 函數(shù) 
  11.     go F() 
  12.     time.Sleep(time.Second
  13.  
  14. func F() { 
  15.     defer func() { 
  16.         fmt.Println("b"
  17.     }() 
  18.     // goroutine 內(nèi)部拋出panic 
  19.     panic("a"
  20.  
  21. func main() { 
  22.     G() 

順序如下:

  1. 調(diào)用 G() 函數(shù);
  2. 通過 goroutine 調(diào)用 F() 函數(shù);
  3. F() 中遇到 panic,立刻終止,不執(zhí)行 panic 之后的代碼;
  4. 執(zhí)行 F() 中 defer 函數(shù),由于沒有 recover,則將 panic 拋到 G() 中;
  5. 由于 goroutine 內(nèi)部沒有進(jìn)行 recover,則 goroutine 外部函數(shù),也就是 G() 函數(shù)是沒辦法捕獲的,程序直接崩潰退出。

輸出:

  1. panic: a 
  2.  
  3. goroutine 6 [running]: 
  4. main.F() 
  5.     xxx.go:96 +0x5b 
  6. created by main.G 
  7.     xxx.go:87 +0x57 
  8. exit status 2 

八、

最后再說一個(gè) recover 的返回值問題:

  1. defer func() { 
  2.     if err := recover(); err != nil { 
  3.         fmt.Println("捕獲異常:", err.Error()) 
  4.     } 
  5. }() 
  6. panic("a"

recover 返回的是 interface {} 類型,而不是 error 類型,所以這樣使用的話會(huì)報(bào)錯(cuò):

  1. err.Error undefined (type interface {} is interface with no methods) 

可以這樣來轉(zhuǎn)換一下:

  1. defer func() { 
  2.     if err := recover(); err != nil { 
  3.         fmt.Println("捕獲異常:", fmt.Errorf("%v", err).Error()) 
  4.     } 
  5. }() 
  6. panic("a"

或者直接打印結(jié)果:

  1. defer func() { 
  2.     if err := recover(); err != nil { 
  3.         fmt.Println("捕獲異常:", err) 
  4.     } 
  5. }() 
  6. panic("a"

輸出:

  1. 捕獲異常: a 

以上就是本文的全部內(nèi)容,其實(shí)寫過其他的語言的同學(xué)都知道,關(guān)閉文件句柄,釋放鎖等操作是很容易忘的。而 Go 語言通過 defer 很好地解決了這個(gè)問題,但在使用過程中還是要小心。

本文總結(jié)了一些容踩坑的點(diǎn),希望能夠幫助大家少寫 BUG,如果大家覺得有用的話,歡迎點(diǎn)贊和轉(zhuǎn)發(fā)。

文章中的腦圖和源碼都上傳到了 GitHub,有需要的同學(xué)可自行下載。

源碼地址:

https://github.com/yongxinz/gopher/tree/main/sc

本文轉(zhuǎn)載自微信公眾號「AlwaysBeta」,可以通過以下二維碼關(guān)注。轉(zhuǎn)載本文請聯(lián)系A(chǔ)lwaysBeta公眾號。

 

責(zé)任編輯:武曉燕 來源: AlwaysBeta
相關(guān)推薦

2025-04-03 12:30:00

C 語言隱式類型轉(zhuǎn)換代碼

2022-07-15 08:20:54

Java基礎(chǔ)知識

2020-10-09 07:54:43

PythonJava爬蟲

2025-04-29 08:30:00

迭代器失效C++編程

2019-10-25 22:17:25

開發(fā)者技能工具

2025-04-21 10:35:37

2020-08-05 07:53:53

程序員網(wǎng)站技術(shù)

2018-02-06 08:36:02

簡歷程序員面試

2015-05-15 10:09:09

程序員

2024-03-26 00:48:38

2020-09-14 08:47:46

緩存程序員存儲

2015-09-16 09:57:41

swoolePHP程序員

2024-04-01 08:05:27

Go開發(fā)Java

2024-03-13 13:10:48

JavaInteger緩存

2023-11-13 08:34:01

Java編程習(xí)慣

2014-08-13 11:11:58

程序員

2013-08-20 09:33:59

程序員

2022-09-25 21:58:27

程序員

2020-10-28 09:43:40

前端開發(fā)Vue

2018-10-11 10:41:12

Go 開發(fā)技術(shù)
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號