Golang筆記:錯誤處理學習筆記
一、進階學習
1.1、錯誤(異常處理)
Go
語言中也有和Java
中的異常處理相關的機制,不過,在Go
里面不叫異常,而是叫做:錯誤
。錯誤分為三類,分別是:
error
:部分流程錯誤,需要程序員進行處理,這種級別的錯誤,不會導致整個程序停止運行。panic
:嚴重錯誤,等程序執行完成之后,會立即退出運行。fatal
:致命錯誤,整個程序立即停止運行。
在Go
語言中,不存在try..catch
相關的語句,Go
中的錯誤是通過返回值的形式返回的,例如:在調用一個方法的時候,這個方法可以增加一個返回值,表示是否存在錯誤信息,然后通過if
條件語句判斷,是否能夠正常執行后續代碼。
上面這種錯誤的處理方式,雖然簡化了try...catch
的使用,但是,引入了if
條件語句,也會導致代碼里面出現一大堆的條件判斷,所以,從本質上來說,還是沒有簡化的,只不過是采用另一種方式處理錯誤而已。
1.1.1、error錯誤
Go
中有一個error
接口,接口里面只有一個Error()
方法,返回值是一個string
字符串,這個字符串表示錯誤信息。Go
中創建error
錯誤,可以通過下面兩種方式:
- 第一種方式:使用
errors
包下的New()
函數。 - 第二種方式:使用
fmt
包下的Errorf()
函數。
package mainimport ("errors""fmt"
)// 返回值第二個參數是 error 錯誤
func div(a int, b int) (int, error) {if b == 0 {// 定義 error 級別的錯誤返回return -1, errors.New("分母不能為0")// 或者使用下面這種方式// return -1, fmt.Errorf("分母不能為0")}c := a / breturn c, nil
}func main() {i, err := div(1, 0)if err != nil {fmt.Println(err)return}fmt.Println("結果=", i)
}
1.1.2、自定義error錯誤
Go
中也允許程序開發人員自定義error
級別的錯誤。自定義錯誤很簡單,只需要對應的類型,實現了Error()
方法,那么這個類型就將被看作是error
接口的實現類,也就屬于error
類型的錯誤了。
自定義error
錯誤的步驟,如下所示:
- 第一步:自定義一個類型。
- 第二步:實現
Error()
方法,返回string
錯誤字符串信息。 - 第三步:創建自定義類型對象,作為
error
錯誤即可。
案例代碼,如下所示:
package mainimport ("fmt"
)// 第一步:MyCustomError 自定義錯誤類型
type MyCustomError struct {code intmsg string
}// 第二步:實現 error 接口的 Error() 方法
func (e MyCustomError) Error() string {// 自定義錯誤的返回格式return fmt.Sprintf("自定義錯誤:code=%d, msg=%s", e.code, e.msg)
}func main() {// 第三步:創建自定義錯誤customError := MyCustomError{code: 1,msg: "分母不能為0",}fmt.Println(customError)
}
1.1.3、Unwrap解包錯誤
Go
語言中,允許錯誤嵌套,一個錯誤嵌套一個錯誤,從而形成一個錯誤鏈表。要想從一個錯誤中,獲取它包含的錯誤,可以調用errors
中提供的Unwrap()
方法。
Unwrap()
方法:從當前error
錯誤中,獲取其內部包含的error
錯誤,如果內部不存在error
錯誤,則返回nil
;如果存在,則返回內部的error
錯誤。
需要注意的是,Unwrap()
方法獲取到的error
錯誤,有可能也是嵌套的error
錯誤,如果想要獲取指定的error
錯誤,可以使用遞歸的方式進行判斷。
package mainimport ("errors""fmt"
)// MyCustomError 自定義錯誤類型
type MyCustomError struct {code intmsg string
}// 實現 error 接口的 Error() 方法
func (e MyCustomError) Error() string {// 自定義錯誤的返回格式return fmt.Sprintf("自定義錯誤:code=%d, msg=%s", e.code, e.msg)
}func main() {// 創建自定義錯誤customError := MyCustomError{code: 1,msg: "分母不能為0",}// 嵌套錯誤,形成一個錯誤鏈err := fmt.Errorf("錯誤:%w", customError)fmt.Println(err)// 解包錯誤err = errors.Unwrap(err)fmt.Println(err)// 解包錯誤err = errors.Unwrap(err)fmt.Println(err)
}
// 執行結果
錯誤:自定義錯誤:code=1, msg=分母不能為0
自定義錯誤:code=1, msg=分母不能為0
<nil>
從上面可以看到,errors.Unwrap()
方法的作用就是從錯誤鏈中解包內部錯誤。
1.1.4、檢查錯誤
Go
語言中,判斷某個error
錯誤是否為指定類型,可以使用errors.Is()
方法,這個方法的作用是:判斷當前error
是否為目標target
類型的錯誤,如果是目標類型的錯誤,則返回true
,否則返回false
。
errors.Is(err, targer error) bool
方法:判斷是否為目標錯誤類型。
package mainimport ("errors""fmt"
)var originalErr = errors.New("this is an error")// 包裹原始錯誤
func wrap1() error {return fmt.Errorf("wrapp error %w", wrap2())
}// 原始錯誤
func wrap2() error {return originalErr
}func main() {err := wrap1()// 如果使用if err == originalErr 將會是falseif errors.Is(err, originalErr) {fmt.Println("original")}
}
另外,Go
中還提供了一個errors.As(err, target Any) bool
方法,這個方法的作用是:在當前錯誤鏈中尋找第一個匹配target
的錯誤類型,并且將找到的錯誤賦值給傳入的err
變量。
errors.As(err, target Any) bool
方法:用于檢查一個錯誤是否可以轉換成目標,如果可以就會將目標錯誤賦值給第一個參數err
。
package mainimport ("errors""fmt""time"
)// TimeError 自定義error
type TimeError struct {Msg string// 記錄發生錯誤的時間Time time.Time
}// 實現 error 接口方法
func (m TimeError) Error() string {return m.Msg
}// NewMyError 定義自定義錯誤函數,并且使用 & 符號,返回一個 結構體指針 類型
func NewMyError(msg string) error {// 使用 & 符號,返回一個 結構體指針 類型,即:返回內存地址return &TimeError{Msg: msg,Time: time.Now(),}
}// 包裹原始錯誤
func wrap1() error {return fmt.Errorf("wrapp error %w", wrap2())
}// 原始錯誤
func wrap2() error {return NewMyError("original error")
}func main() {// 定義一個 結構體指針 變量var myerr *TimeError// 獲取錯誤err := wrap1()// 檢查錯誤鏈中是否有 *TimeError 類型的錯誤if errors.As(err, &myerr) { // 輸出TimeError的時間fmt.Println("original", myerr.Time)}
}
1.2、panic
前面學的error
錯誤,是不嚴重的錯誤類型,如果不在后面寫return
語句,那么程序還是會繼續往后執行下去的。Go
語言中還提供了一個panic
錯誤。
panic
類型是一種比error
更加嚴重的錯誤類型,當出現panic
之后,后續的代碼時不會繼續執行
,即使沒有使用return
關鍵字,代碼也不會執行panic
后面的代碼。
注意:
panic
你可以理解成是Java
語言中的使用throw new
拋出異常的模式。
1.2.1、創建panic
Go
中提供了一個panic
函數,通過這個函數可以創建panic
錯誤。
func panic(v any)
我對panic
的理解是,panic
就相當于是簡寫了Java
語言中的throw new Exception("錯誤信息")
的語句。例如:
// 在Java語言中,拋出異常
throw new Exception("錯誤異常信息")// 而在Go里面,只需要寫一個 panic 函數即可
panic("錯誤異常信息")
使用panic
的時候,需要注意的是,panic
后面的代碼是不會執行的。
1.2.2、panic善后
當發生panic
錯誤之前,如果存在defer
延遲函數,那么程序首先會依次執行defer
延遲函數,執行完成之后,才會觸發panic
。
package mainimport "fmt"func mockPanic() {fmt.Println("開始執行panic...")panic("模擬panic錯誤...")fmt.Println("panic執行結束...")
}
func demo01() {fmt.Println("執行demo01()函數...")
}
func demo02() {fmt.Println("執行demo02()函數...")
}
func demo03() {fmt.Println("執行demo03()函數...")
}func main() {fmt.Println("Hello")defer demo01()defer demo02()mockPanic()defer demo03()fmt.Println("World")
}// 程序運行結果
Hello
開始執行panic...
執行demo02()函數...
執行demo01()函數...
panic: 模擬panic錯誤...goroutine 1 [running]:
main.mockPanic()D:/environment/GoWorks/src/go-study/Hello.go:7 +0x59
main.main()D:/environment/GoWorks/src/go-study/Hello.go:24 +0x7d
當發生panic
時,會立即停止當前函數的執行,然后執行善后的一些操作,例如:執行defer
延遲函數;執行完成之后,會將panic
向上層函數傳遞,上層函數也會進行相應的善后操作,直到main
函數終止運行。
1.2.3、恢復執行
當使用panic
之后,程序會終止運行,如果我們想恢復程序的執行,那么可以使用recover()
內置函數來實現。需要注意的是,recover()
函數必須在defer
延遲函數里面使用。另外,recover()
函數的返回值是panic
函數中的錯誤信息內容。
package mainimport ("fmt"
)func demo() {fmt.Println("C")defer func() {fmt.Println("D")// 恢復程序執行,就相當于是取消panicok := recover()if ok != nil {fmt.Println("恢復程序執行")}fmt.Println("E")}()// 發生 panic 則 demo 方法終止執行panic("模擬panic錯誤...")fmt.Println("F")
}func main() {fmt.Println("A")demo()fmt.Println("B")
}// 執行結果
A
C
D
恢復程序執行
E
B
從上面代碼中,可以發現,正常情況下,發生panic
之后,如果沒有使用recover()
函數恢復執行,那么最終的輸出結果將只有下面這些內容:
A
C
D
E
panic: 模擬panic錯誤...goroutine 1 [running]:
main.demo()D:/environment/GoWorks/src/go-study/Hello.go:19 +0x76
main.main()D:/environment/GoWorks/src/go-study/Hello.go:25 +0x4f
但是,我們在demo()
函數中,定義了一個defer
延遲函數,并且在里面使用recover()
函數恢復程序的執行,也就相當于是捕獲處理了panic
,那么demo()
函數中的panic就不會向上傳遞到main
函數里面,所以main
函數中的代碼將正常執行。
理解這一點很重要,因為如果把上面代碼改成下面執行順序,如下所示:
package mainimport ("fmt"
)func demo() {fmt.Println("C")// 發生 panic 則 demo 方法終止執行panic("模擬panic錯誤...")fmt.Println("F")
}func main() {fmt.Println("A")// 在main函數里面定義deferdefer func() {fmt.Println("D")// 恢復程序執行,就相當于是取消panicok := recover()if ok != nil {fmt.Println("恢復程序執行")}fmt.Println("E")}()demo()fmt.Println("B")
}// 執行結果
A
C
D
恢復程序執行
E
上面代碼中,從輸出結果來看,程序沒有執行demo()
函數之后的代碼,這是為什么呢???
- 我是這么理解的,當
demo()
函數中,發生panic
之后,由于demo()
函數沒有進行處理,所以會向上傳遞到main()
函數里面。 main()
函數發現,此時發生了panic
,所以就不會執行后面的代碼。- 此時觸發
panic
的善后相關代碼,例如:執行defer
函數。 - 首先,會從
demo()
函數中,依次開始執行defer
函數,然后向上傳遞到main
函數里面,開始執行main
函數中的defer
函數。 defer
函數執行完成之后,此時整個程序就結束運行了。- 所以,最終
demo()
函數之后的代碼,也就不會再執行了。
下面舉個例子,來看看發生panic之后,程序的輸出結果分別是多少,如下所示:
package mainimport ("fmt"
)func demo() {fmt.Println("B1")defer fmt.Println("C")defer func() {fmt.Println("E1")// 恢復程序執行,就相當于是取消panicok := recover()if ok != nil {fmt.Println("恢復程序執行")}fmt.Println("E2")}()demo02()fmt.Println("B2")
}func demo02() {fmt.Println("D1")panic("demo02函數發生panic...")fmt.Println("D2")
}func main() {fmt.Println("A1")demo()fmt.Println("A2")
}// 輸出結果
// A1 B1 D1 E1 恢復... E2 C A2
看到這里,你學會了嗎???
使用recover()
函數,有四個注意事項:
- 必須在
defer
函數中使用recover()
函數。 - 多次使用
recover()
函數,只會恢復一個panic
。 - 閉包結構中使用
recover()
函數,不能恢復外部函數的panic
。 panic
參數禁止使用nil
。
可以將recover()
函數理解成是Java
中的try...catch
的功能,也就是捕獲panic
錯誤,然后能夠讓程序繼續正常執行。
1.3、fatal
fatal
是Go
語言中的致命錯誤,這種錯誤一旦發生,那么整個程序就會立即停止運行,fatal
是沒有辦法進行善后操作的,也就是說,發生fatal
之后,程序都來不及執行defer
函數。
package mainimport ("fmt""os"
)func main() {fmt.Println("A1")// 模擬 fatal 錯誤os.Exit(1)fmt.Println("A2")// A1 B1 D1 E1 恢復 E2 C A2
}
Go
語言中一般不會顯示的聲明fatal
錯誤,發生fatal
錯誤一般都是程序自主發生的,不是人為干預的。
注意:
Go
語言中一般使用os.Exit(1)
代碼來實現fatal
錯誤。
以上,就是Go
語言中錯誤處理相關的知識點。