從 Python try except 的角度理解 Go 如何用 defer panic() recover() 處理例外/錯誤
Posted on Mar 27, 2024 in Go 程式設計 - 初階 by Amo Chen ‐ 5 min read
學習 Go 的過程一定有人跟我一樣,對於 Go 用 defer(), panic(), recover() 處理例外/錯誤的方式感到困惑,特別是已經習慣使用 Java, JavaScript, Python 等程式語言的開發者來說, Java, JavaScript, Python 所提供的 try...catch
, try-except
在可讀性相對友善很多之外,在開發時也直覺很多。
但要理解 Go 的 defer(), panic(), recover() 其實可以從已知的模式出發,如此原本無法輕易理解的事物,就會變成好理解許多。
本文將從 Python 的 try-except
出發,學習 Go 如何做到相同的例外(exception)處理。
本文環境
- Python 3
- Go
從 Python 的 try-except 開始到 Go 的 defer, panic(), recover()
Python 的 try-except
可以捕捉 try
區塊內的程式碼所拋出的例外(exception)或錯誤,並在捕捉到例外(exception)或錯誤之後,執行 except
區塊內的程式碼,因此以下 Python 的錯誤處理模式相當常見:
try:
# 可能發生錯誤的程式碼
# 可能發生錯誤的程式碼
except Exception as e:
if isinstance(e, ValueError):
# 進行錯誤處理
else:
# 進行錯誤處理
例如,下列範例函式直接在 try
區塊拋出 ValueError
的錯誤,讓 except
區塊捕捉到例外之後,就印出 f'Got an exception: {e}'
字串:
def example():
try:
raise ValueError()
except Exception as e:
print(f'Got an exception: {e}')
如果轉換為 Go 的寫法將會如下所示:
package main
import "fmt"
func example() {
defer func() {
if r := recover(); r != nil {
fmt.Println("Got an exception:", r)
}
}()
panic("value error")
}
func main() {
example()
}
畫成圖會更容易理解 Python, Go 兩者相同作用的部分:
Go 的 panic()
其實作用等同於 Python 的 raise
,拋出 1 個例外/錯誤的意思;拋出例外/錯誤之後,就必須要有地方捕捉才有辦法進一步做處置,這個捕捉的方法所對應的就是 recover()
。
至於為什麼需要 recover()
一定要放在 defer
函式中,則是因為 Go 的規定:
Recover is a built-in function that regains control of a panicking goroutine. Recover is only useful inside deferred functions. During normal execution, a call to recover will return nil and have no other effect. If the current goroutine is panicking, a call to recover will capture the value given to panic and resume normal execution.
如果不是在 defer
函式中呼叫 recover()
,或者沒有 panic 發生, recover()
也只會直接回傳 nil
。
另外是 defer
函式的特性是在呼叫者(caller)執行 return
之前執行,也因為如此,如果 1 個函式在 return
之前發生 panic, derfer
函式內的 recover()
才能夠正確捕捉例外/錯誤:
package main
import (
"fmt"
)
func caller() {
defer func() {
if r := recover(); r != nil {
// error handling
fmt.Println("error handling")
}
}()
fmt.Println("error happened")
panic("error happened")
}
func main() {
caller()
}
至此,我們應能理解 defer
, panic()
, recover()
三者搭配的作用:
panic()
拋出例外/錯誤defer
+recover()
捕捉例外/錯誤
其他例外處理的常見模式
前文已經從 Python 的 try-except
角度理解 defer
, panic()
, recover()
三者搭配的作用,不過對於學習 Go 的新手來說,也許仍有常見的例外處理模式無法直覺地反應,以下將逐一破解。
無論如何都要回傳值的情況
在有些情況下,有些錯誤其實不需要理會,只需要針對錯誤作出紀錄即可,例如下列 Python 程式碼,我們僅對錯誤列印警告訊息,之後便正常回傳 1 個值:
def get_value_no_matter_what_happened():
try:
raise ValueError()
except Exception as e:
print('Warning: there is an error here')
return 1
如果轉換為 Go 的寫法,就不像上述寫法般直覺,原因在於呼叫 panic()
之後,我們就不能再回到原本程式的流程,例如下列範例 fmt.Println("Hello")
是永遠不會被執行的:
package main
import "fmt"
func main() {
defer func() {
if r := recover(); r != nil {
fmt.Println("error handling")
}
}()
panic("error")
fmt.Println("Hello") // <- unreachable code
}
要解決前述問題的話,可以使用 named return values, 只要在函式定義時給回傳值指定 1 個命名,我們即可在 defer 函式中修改回傳值,下列範例中的 (ret int)
就是 named return value 的用法:
package main
import "fmt"
func getValueNoMatterWhatHappened() (ret int) {
defer func() {
if r := recover(); r != nil {
fmt.Println("Warning: error here")
ret = 1
}
}()
panic("value error")
}
func main() {
fmt.Println("getValueNoMatterWhatHappened:", getValueNoMatterWhatHappened())
}
上述範例執行結果如下,可以看到即使 getValueNoMatterWhatHappened()
發生 panic, 我們仍能藉由 named return values 的做法正常回傳值:
Warning: error here
getValueNoMatterWhatHappened: 1
這一切都要歸功於 defer
函式具有合法讀取、修改回傳值的能力:
Deferred functions may read and assign to the returning function’s named return values.
所以我說那個 finally 區塊呢?
A finally clause is always executed before leaving the try statement, whether an exception has occurred or not.
Python 的 try-except
還有個 finally
區塊可以使用, finally
區塊會在離開 try-except
區塊時執行,無論是否有任何例外發生。官方說明 finally
區塊相當適合作為釋放資源的用途使用,例如釋放 lock 避免死結(deadlock)產生。
以下是 1 個 Python try-except-finally
的範例:
def get_value_no_matter_what_happened():
try:
raise ValueError()
except Exception as e:
print('the exception block executed')
return 1
finally:
print('the finally block executed')
上述執行結果如下,可以看到即使 return 1
已經被執行了, finally
區塊仍會運作:
>>> x = get_value_no_matter_what_happened()
the exception block executed
the finally block executed
>>> x
1
Go 也能做到類似的事,簡單來說,只要在 defer 函式內再多加 1 個 defer 函式即可:
package main
import "fmt"
func getValueNoMatterWhatHappened() (ret int) {
defer func() {
defer func() {
fmt.Println("the finally block executed")
}()
if r := recover(); r != nil {
fmt.Println("the exception block executed")
ret = 1
}
}()
panic("value error")
}
func main() {
fmt.Println("getValueNoMatterWhatHappened:", getValueNoMatterWhatHappened())
}
p.s. 你也可以在最裡層的 defer 函式再次修改 ret
的值,作用跟在 Python 的 finally
區塊內 return
一樣,都對回傳值有作用
讓 flow 繼續下去
前面說到 panic()
呼叫之後,我們就無法回到原有的程式流程,如果以 Python 的 try-except
來看,它只要在 except
捕捉到錯誤,並做出處理之後,是可以繼續程式的流程的,如下列範例中的 print('do the rest of the jobs')
在錯誤發生之後仍能執行,因為我們已經使用 try-except
捕捉並且處理錯誤:
def do_something_no_matter_what_happened():
try:
print('do something')
raise ValueError()
except Exception as e:
print('do something if an error occurred')
print('do the rest of the jobs')
針對這種問題,其實 Go 的函式可以回傳 1 個值代表是否有錯誤發生,只要針對該錯誤進行判斷並處理即可,例如下列模式在 Go 程式碼中相當常見:
err := doX()
if err != nil {
// error handling
}
所以前述 Python 範例等價於下列 Go 程式碼:
package main
import (
"errors"
"fmt"
)
func doSomething() error {
fmt.Println("do something")
return errors.New("value error")
}
func getValueNoMatterWhatHappened() {
err := doSomething()
if err != nil {
fmt.Println("do something if an error occurred")
}
fmt.Println("do the rest of the jobs")
}
func main() {
getValueNoMatterWhatHappened()
}
不過,對我們而言 doSomething()
要在函式內處理錯誤,還是呼叫 doSomething()
的地方處理錯誤,其實是可以選擇的。
err := doSomething()
if err != nil {
fmt.Println("do something if an error occurred")
}
我們也可以把會發生錯誤的上述程式碼,抽取至該函式內自行處理錯誤即可:
package main
import "fmt"
func doSomething() {
defer func() {
if r := recover(); r != nil {
fmt.Println("do something if an error occurred")
}
}()
fmt.Println("do something")
panic("value error")
}
func getValueNoMatterWhatHappened() {
doSomething()
fmt.Println("do the rest of the jobs")
}
func main() {
getValueNoMatterWhatHappened()
}
上述範例執行結果如下,可以看到 2 種不同做法都能達到訴求:
do something
do something if an error occurred
do the rest of the jobs
總結
Go 的 defer, panic(), recover() 的設計雖然跟 Java, JavaScript, Python 等程式語言不同,但都能做到相同的事,只是對於已經慣用一套既定模式的我們而已,我們更需要從已知的範例開始延伸研究,以幫助我們更加深入地認識 Go 的 defer, panic(), recover() 的設計而已。
以上!
Enjoy!
References
Defer, Panic, and Recover - The Go Programming Language