本文為轉(zhuǎn)載倍宾,原文:Golang 學習筆記(07)—— 錯誤及異常處理
基礎知識
錯誤指的是可能出現(xiàn)問題的地方出現(xiàn)了問題高职,比如打開一個文件時失敗初厚,這種情況在人們的意料之中 孙技;而異常指的是不應該出現(xiàn)問題的地方出現(xiàn)了問題牵啦,比如引用了空指針,這種情況在人們的意料之外楞件⊥两可見,錯誤是業(yè)務過程的一部分泪酱,而異常不是 还最。
Golang中引入error接口類型作為錯誤處理的標準模式拓轻,如果函數(shù)要返回錯誤,則返回值類型列表中肯定包含error勿锅。error處理過程類似于C語言中的錯誤碼枣氧,可逐層返回作瞄,直到被處理宗挥。
Golang中引入兩個內(nèi)置函數(shù)panic和recover來觸發(fā)和終止異常處理流程种蝶,同時引入關鍵字defer來延遲執(zhí)行defer后面的函數(shù)。
一直等到包含defer語句的函數(shù)執(zhí)行完畢時搪桂,延遲函數(shù)(defer后的函數(shù))才會被執(zhí)行盯滚,而不管包含defer語句的函數(shù)是通過return的正常結(jié)束魄藕,還是由于panic導致的異常結(jié)束。你可以在一個函數(shù)中執(zhí)行多條defer語句话瞧,它們的執(zhí)行順序與聲明順序相反交排。
當程序運行時,如果遇到引用空指針处坪、下標越界或顯式調(diào)用panic函數(shù)等情況稻薇,則先觸發(fā)panic函數(shù)的執(zhí)行,然后調(diào)用延遲函數(shù)胶征。調(diào)用者繼續(xù)傳遞panic塞椎,因此該過程一直在調(diào)用棧中重復發(fā)生:函數(shù)停止執(zhí)行,調(diào)用延遲執(zhí)行函數(shù)等睛低。如果一路在延遲函數(shù)中沒有recover函數(shù)的調(diào)用案狠,則會到達該協(xié)程的起點,該協(xié)程結(jié)束钱雷,然后終止其他所有協(xié)程骂铁,包括主協(xié)程。
Golang錯誤和異常是可以互相轉(zhuǎn)換的:
- 錯誤轉(zhuǎn)異常罩抗,比如程序邏輯上嘗試請求某個URL拉庵,最多嘗試三次套蒂,嘗試三次的過程中請求失敗是錯誤钞支,嘗試完第三次還不成功的話,失敗就被提升為異常了操刀。
- 異常轉(zhuǎn)錯誤烁挟,比如panic觸發(fā)的異常被recover恢復后,將返回值中error類型的變量進行賦值骨坑,以便上層函數(shù)繼續(xù)走錯誤處理流程撼嗓。
實例
package main
import (
"errors"
"fmt"
)
func main(){
defer func(){
if err := recover(); err != nil{ //捕捉異常并處理
fmt.Println("err: ", err)
}
}()
if num, err := delive(20, -5); err == nil{
fmt.Printf("%f / %f = %f\n", 20.0, -5.0, num)
}else{
fmt.Println(err)
}
if num, err := delive(20, 0); err == nil{
fmt.Printf("%f / %f = %f\n", 20.0, 0.0, num)
}else{
fmt.Println(err)
}
}
func delive(numA, numB float32) (float32, error){
if numB < 0{
return 0, errors.New("被除數(shù)不能為負數(shù)")
}else if numB == 0{
panic("被除數(shù)不能為0") //拋出異常
}else{
return numA/numB, nil
}
}
error 類型實際上是抽象了 Error() 方法的 error 接口,Golang 使用該接口進行標準的錯誤處理欢唾。
Go中可以拋出一個panic的異常且警,然后在defer中通過recover捕獲這個異常,然后正常處理礁遣。
error使用場景
失敗原因只有一個時振湾,不應該使用error
當函數(shù)失敗的原因只有一個,所以返回值的類型應該為bool亡脸,而不是error押搪。大多數(shù)情況树酪,導致失敗的原因不止一種,尤其是對I/O操作而言大州,用戶需要了解更多的錯誤信息续语,這時的返回值類型不再是簡單的bool,而是error厦画。沒有失敗時疮茄,不使用error
error在Golang中是如此的流行,以至于很多人設計函數(shù)時不管三七二十一都使用error根暑,即使沒有一個失敗原因力试。當沒有失敗原因時,使用無返回的函數(shù)會更加合理排嫌。error應放在返回值類型列表的最后
對于返回值類型error畸裳,用來傳遞錯誤信息,在Golang中通常放在最后一個淳地。bool作為返回值類型時也一樣怖糊。錯誤值統(tǒng)一定義,而不是跟著感覺走
很多人寫代碼時颇象,到處return errors.New(value)伍伤,而錯誤value在表達同一個含義時也可能形式不同。
這使得相同的錯誤value撒在一大片代碼里遣钳,當上層函數(shù)要對特定錯誤value進行統(tǒng)一處理時扰魂,需要漫游所有下層代碼,以保證錯誤value統(tǒng)一蕴茴,不幸的是有時會有漏網(wǎng)之魚劝评,而且這種方式嚴重阻礙了錯誤value的重構。
于是荐开,我們可以在Golang的每個包中增加一個錯誤對象定義文件付翁,如下所示:
var ERR_EOF = errors.New("EOF")
var ERR_CLOSED_PIPE = errors.New("io: read/write on closed pipe")
var ERR_NO_PROGRESS = errors.New("multiple Read calls return no data or error")
var ERR_SHORT_BUFFER = errors.New("short buffer")
var ERR_SHORT_WRITE = errors.New("short write")
var ERR_UNEXPECTED_EOF = errors.New("unexpected EOF")
錯誤逐層傳遞時简肴,層層都加日志
層層都加日志非常方便故障定位晃听。錯誤處理使用defer
我們一般通過判斷error的值來處理錯誤,如果當前操作失敗砰识,需要將本函數(shù)中已經(jīng)create的資源destroy掉能扒,示例代碼如下:
func deferDemo() error {
err := createResource1()
if err != nil {
return ERR_CREATE_RESOURCE1_FAILED
}
err = createResource2()
if err != nil {
destroyResource1()
return ERR_CREATE_RESOURCE2_FAILED
}
err = createResource3()
if err != nil {
destroyResource1()
destroyResource2()
return ERR_CREATE_RESOURCE3_FAILED
}
err = createResource4()
if err != nil {
destroyResource1()
destroyResource2()
destroyResource3()
return ERR_CREATE_RESOURCE4_FAILED
}
return nil
}
當Golang的代碼執(zhí)行時鹉胖,如果遇到defer的閉包調(diào)用酒朵,則壓入堆棧。當函數(shù)返回時永淌,會按照后進先出的順序調(diào)用閉包膨处。
對于閉包的參數(shù)是值傳遞见秤,而對于外部變量卻是引用傳遞砂竖,所以閉包中的外部變量err的值就變成外部函數(shù)返回時最新的err值。
根據(jù)這個結(jié)論鹃答,我們重構上面的示例代碼:
func deferDemo() error {
err := createResource1()
if err != nil {
return ERR_CREATE_RESOURCE1_FAILED
}
defer func() {
if err != nil {
destroyResource1()
}
}()
err = createResource2()
if err != nil {
return ERR_CREATE_RESOURCE2_FAILED
}
defer func() {
if err != nil {
destroyResource2()
}
}()
err = createResource3()
if err != nil {
return ERR_CREATE_RESOURCE3_FAILED
}
defer func() {
if err != nil {
destroyResource3()
}
}()
err = createResource4()
if err != nil {
return ERR_CREATE_RESOURCE4_FAILED
}
return nil
}
- 當上層函數(shù)不關心錯誤時乎澄,建議不返回error
對于一些資源清理相關的函數(shù)(destroy/delete/clear),如果子函數(shù)出錯测摔,打印日志即可置济,而無需將錯誤進一步反饋到上層函數(shù),因為一般情況下锋八,上層函數(shù)是不關心執(zhí)行結(jié)果的浙于,或者即使關心也無能為力,于是我們建議將相關函數(shù)設計為不返回error挟纱。
異常的使用場景
在程序開發(fā)階段羞酗,堅持速錯
速錯,簡單來講就是“讓它掛”樊销,只有掛了你才會第一時間知道錯誤整慎。在早期開發(fā)以及任何發(fā)布階段之前,最簡單的同時也可能是最好的方法是調(diào)用panic函數(shù)來中斷程序的執(zhí)行以強制發(fā)生錯誤围苫,使得該錯誤不會被忽略裤园,因而能夠被盡快修復。在程序部署后剂府,應恢復異常避免程序終止
一旦Golang程序部署后拧揽,在任何情況下發(fā)生的異常都不應該導致程序異常退出,我們在上層函數(shù)中加一個延遲執(zhí)行的recover調(diào)用來達到這個目的腺占,并且是否進行recover需要根據(jù)環(huán)境變量或配置文件來定淤袜,默認需要recover。
我們在調(diào)用recover的延遲函數(shù)中以最合理的方式響應該異常:
打印堆棧的異常調(diào)用信息和關鍵的業(yè)務信息衰伯,以便這些問題保留可見铡羡;
將異常轉(zhuǎn)換為錯誤,以便調(diào)用者讓程序恢復到健康狀態(tài)并繼續(xù)安全運行意鲸。
對于不應該出現(xiàn)的分支烦周,使用異常處理
當某些不應該發(fā)生的場景發(fā)生時,我們就應該調(diào)用panic函數(shù)來觸發(fā)異常怎顾。比如读慎,當程序到達了某條邏輯上不可能到達的路徑針對入?yún)⒉粦撚袉栴}的函數(shù),使用panic設計
對于同時支持用戶輸入場景和硬編碼場景的情況槐雾,一般支持硬編碼場景的函數(shù)是對支持用戶輸入場景函數(shù)的包裝夭委。
對于只支持硬編碼單一場景的情況,函數(shù)設計時直接使用panic募强,即返回值類型列表中不會有error株灸,這使得函數(shù)的調(diào)用處理非常方便
完
轉(zhuǎn)載請注明出處:
Golang 學習筆記(07)—— 錯誤及異常處理
目錄
上一節(jié):Golang 學習筆記(06)—— 多線程
下一節(jié):Golang 學習筆記(08)—— 文件操作