【go語言學習】錯誤error和異常panic
一、錯誤和異常的區別
錯誤指的是可能出現問題的地方出現了問題。比如開啟一個檔案時失敗,這種情況在人們的意料之中 。
異常指的是不應該出現問題的地方出現了問題。比如引用了空指標,這種情況在人們的意料之外。
可見,錯誤是業務過程的一部分,而異常不是 。
二、錯誤演示
go語言中,錯誤是一種資料型別,使用內建的error
型別,和其他資料型別一樣使用。
package main import ( "fmt" "os" ) func main() { _, err := os.Open("a.txt") if err != nil { fmt.Println(err) return } fmt.Println("open file success") }
執行結果
open a.txt: The system cannot find the file specified.
三、錯誤型別
go語言通過內建的error
介面提供了非常簡單的錯誤處理機制。
type error interface {
Error() string
}
這個介面中包含一個Error() string
,任何實現了該方法的型別都可以作為一個錯誤使用。這個方法提供了錯誤的描述。
列印錯誤是,fmt.Println
函式在內部呼叫Error()
方法來獲取錯誤的描述。
從錯誤中提取更多資訊的方法:
1、斷言底層結構體型別並從結構體欄位中獲取更多的資訊。
檢視go語言原始碼,可以發現上面的示例中開啟檔案的程式碼中返回值error
error
本身是PathError
結構體型別。
go語言原始碼:
// PathError records an error and the operation and file path that caused it. type PathError struct { Op string Path string Err error } func (e *PathError) Error() string { return e.Op + " " + e.Path + ": " + e.Err.Error() } func (e *PathError) Unwrap() error { return e.Err } // Timeout reports whether this error represents a timeout. func (e *PathError) Timeout() bool { t, ok := e.Err.(timeout) return ok && t.Timeout() }
通過型別斷言,可以將error
作為PathError
型別的例項,訪問其屬性和方法。
示例:
package main
import (
"fmt"
"os"
)
func main() {
_, err := os.Open("a.txt")
if err != nil {
if err, ok := err.(*os.PathError); ok {
fmt.Println(err.Op) // open
fmt.Println(err.Path) // a.txt
fmt.Println(err.Err) // The system cannot find the file specified.
}
return
}
fmt.Println("open file success")
}
2、斷言底層結構體型別,呼叫其方法獲取更多資訊
go語言原始碼:
type DNSError struct {
...
}
func (e *DNSError) Error() string {
...
}
func (e *DNSError) Timeout() bool {
...
}
func (e *DNSError) Temporary() bool {
...
}
DNSError struct有兩個方法Timeout() bool和Temporary() bool,它們返回一個布林值,表示錯誤是由於超時還是臨時的。
示例:
package main
import (
"fmt"
"net"
)
func main() {
add, err := net.LookupHost("www.baiddfudfsadf.com")
if err != nil {
if err, ok := err.(*net.DNSError); ok {
if err.Timeout() {
fmt.Println("operation time out")
} else if err.Temporary() {
fmt.Println("temporary error")
} else {
fmt.Println("genration error", err)
}
}
return
}
fmt.Println(add)
}
執行結果
genration error lookup www.baiddfudfsadf.com: no such host
四、自定義錯誤
1、使用errors
包下的New()
函式
go語言原始碼:
// Package errors implements functions to manipulate errors.
package errors
// New returns an error that formats as the given text.
func New(text string) error {
return &errorString{text}
}
// errorString is a trivial implementation of error.
type errorString struct {
s string
}
func (e *errorString) Error() string {
return e.s
}
示例:
package main
import (
"errors"
"fmt"
"math"
)
func main() {
s := -4.2
res, err := squareArea(s)
if err != nil {
fmt.Println(err)
return
}
fmt.Println("squareArea =", res)
}
func squareArea(sideLength float64) (float64, error) {
if sideLength < 0 {
return 0, errors.New("sideLength < 0")
}
return math.Pow(sideLength, 2), nil
}
執行結果
sideLength < 0
2、使用fmt
包下的Errorf()
函式
go語言原始碼:
func Errorf(format string, a ...interface{}) error {}
示例:
package main
import (
"fmt"
"math"
)
func main() {
s := -4.2
res, err := squareArea(s)
if err != nil {
fmt.Println(err)
return
}
fmt.Println("squareArea =", res)
}
func squareArea(sideLength float64) (float64, error) {
if sideLength < 0 {
return 0, fmt.Errorf("sideLength < 0, sideLength = %v\n", sideLength)
}
return math.Pow(sideLength, 2), nil
}
執行結果
sideLength < 0, sideLength = -4.2
3、使用struct型別屬性和方法提供更多關於錯誤的資訊。
示例:
package main
import (
"fmt"
)
// areaError 結構體 錯誤型別
type areaError struct {
err string
length float64
width float64
}
// Error() 方法,實現error介面
func (e *areaError) Error() string {
return fmt.Sprintf("length: %v, width: %v, error: %v", e.length, e.width, e.err)
}
func (e *areaError) lengthNegative() bool {
return e.length < 0
}
func (e *areaError) widthNegative() bool {
return e.width < 0
}
func main() {
length := -10.2
width := 3.4
res, err := rectangleArea(length, width)
if err != nil {
fmt.Println("err:", err)
if err, ok := err.(*areaError); ok {
if err.lengthNegative() {
fmt.Println("length < 0", err.length)
}
if err.widthNegative() {
fmt.Println("width < 0", err.width)
}
}
}
fmt.Println("rectangle area =", res)
}
func rectangleArea(length, width float64) (float64, error) {
err := ""
if length < 0 {
err += "length < 0"
}
if width < 0 {
if length < 0 {
err += "&width < 0"
} else {
err = "width < 0"
}
}
if err != "" {
return 0, &areaError{err, length, width}
}
return length * width, nil
}
執行結果
err: length: -10.2, width: 3.4, error: length < 0
length < 0 -10.2
rectangle area = 0
五、panic和recover
Golang中引入兩個內建函式panic和recover來觸發和終止異常處理流程,同時引入關鍵字defer來延遲執行defer後面的函式。
panic:
- 1、內建函式
- 2、假如函式F中書寫了panic語句,會終止其後要執行的程式碼,在panic所在函式F內如果存在要執行的defer函式列表,按照defer的逆序執行
- 3、返回函式F的呼叫者G,在G中,呼叫函式F語句之後的程式碼不會執行,假如函式G中存在要執行的defer函式列表,按照defer的逆序執行,這裡的defer 有點類似 try-catch-finally 中的 finally
- 4、直到goroutine整個退出,並報告錯誤
recover:
- 1、內建函式
- 2、用來控制一個goroutine的panicking行為,捕獲panic,從而影響應用的行為
- 3、一般的呼叫建議
a). 在defer函式中,通過recever來終止一個gojroutine的panicking過程,從而恢復正常程式碼的執行
b). 可以獲取通過panic傳遞的error
以下給出異常處理的作用域(場景):
- 空指標引用
- 下標越界
- 除數為0
- 不應該出現的分支,比如default
- 輸入不應該引起函式錯誤
六、錯誤處理的正確姿勢
姿勢一:失敗的原因只有一個時,不使用error
我們看一個案例:
func (self *AgentContext) CheckHostType(host_type string) error {
switch host_type {
case "virtual_machine":
return nil
case "bare_metal":
return nil
}
return errors.New("CheckHostType ERROR:" + host_type)
}
我們可以看出,該函式失敗的原因只有一個,所以返回值的型別應該為bool,而不是error,重構一下程式碼:
func (self *AgentContext) IsValidHostType(hostType string) bool {
return hostType == "virtual_machine" || hostType == "bare_metal"
}
說明:大多數情況,導致失敗的原因不止一種,尤其是對I/O操作而言,使用者需要了解更多的錯誤資訊,這時的返回值型別不再是簡單的bool,而是error。
姿勢二:沒有失敗時,不使用error
錯誤示例:
func (self *CniParam) setTenantId() error {
self.TenantId = self.PodNs
return nil
}
正確示例:
func (self *CniParam) setTenantId() {
self.TenantId = self.PodNs
}
姿勢三:error應該放在返回值列表的最後
對於返回值型別error,用來傳遞錯誤資訊,在Golang中通常放在最後一個。
resp, err := http.Get(url)
if err != nil {
return nill, err
}
bool作為返回值型別時也一樣。
value, ok := cache.Lookup(key)
if !ok {
// ...cache[key] does not exist…
}
姿勢四:錯誤統一定義,而不是跟著感覺走
很多人寫程式碼時,到處return errors.New(value),而錯誤value在表達同一個含義時也可能形式不同,比如“記錄不存在”的錯誤value可能為:
- "record is not existed."
- "record is not exist!"
- "###record is not existed!!!"
- ...
姿勢五:錯誤逐層傳遞時,層層都加日誌
層層都加日誌非常方便故障定位。
說明:至於通過測試來發現故障,而不是日誌,目前很多團隊還很難做到。如果你或你的團隊能做到,那麼請忽略這個姿勢。
姿勢六:錯誤處理使用defer
我們一般通過判斷error的值來處理錯誤,如果當前操作失敗,需要將本函式中已經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的程式碼執行時,如果遇到defer的閉包呼叫,則壓入堆疊。當函式返回時,會按照後進先出的順序呼叫閉包。
對於閉包的引數是值傳遞,而對於外部變數卻是引用傳遞,所以閉包中的外部變數err的值就變成外部函式返回時最新的err值。
根據這個結論,我們重構上面的示例程式碼:
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
}
姿勢七:當嘗試幾次可以避免失敗時,不要立即返回錯誤
如果錯誤的發生是偶然性的,或由不可預知的問題導致。一個明智的選擇是重新嘗試失敗的操作,有時第二次或第三次嘗試時會成功。在重試時,我們需要限制重試的時間間隔或重試的次數,防止無限制的重試。
兩個案例:
- 我們平時上網時,嘗試請求某個URL,有時第一次沒有響應,當我們再次重新整理時,就有了驚喜。
- 團隊的一個QA曾經建議當Neutron的attach操作失敗時,最好嘗試三次,這在當時的環境下驗證果然是有效的。
姿勢八:當上層函式不關心錯誤時,建議不返回error
對於一些資源清理相關的函式(destroy/delete/clear),如果子函數出錯,列印日誌即可,而無需將錯誤進一步反饋到上層函式,因為一般情況下,上層函式是不關心執行結果的,或者即使關心也無能為力,於是我們建議將相關函式設計為不返回error。
姿勢九:當發生錯誤時,不忽略有用的返回值
通常,當函式返回non-nil的error時,其他的返回值是未定義的(undefined),這些未定義的返回值應該被忽略。然而,有少部分函式在發生錯誤時,仍然會返回一些有用的返回值。比如,當讀取檔案發生錯誤時,Read函式會返回可以讀取的位元組數以及錯誤資訊。對於這種情況,應該將讀取到的字串和錯誤資訊一起打印出來。
說明:對函式的返回值要有清晰的說明,以便於其他人使用。
七、異常處理的正確姿勢
姿勢一:在程式開發階段,堅持速錯
速錯,簡單來講就是“讓它掛”,只有掛了你才會第一時間知道錯誤。在早期開發以及任何釋出階段之前,最簡單的同時也可能是最好的方法是呼叫panic函式來中斷程式的執行以強制發生錯誤,使得該錯誤不會被忽略,因而能夠被儘快修復。
姿勢二:在程式部署後,應恢復異常避免程式終止
在Golang中,某個Goroutine如果panic了,並且沒有recover,那麼整個Golang程序就會異常退出。所以,一旦Golang程式部署後,在任何情況下發生的異常都不應該導致程式異常退出,我們在上層函式中加一個延遲執行的recover呼叫來達到這個目的,並且是否進行recover需要根據環境變數或配置檔案來定,預設需要recover。
這個姿勢類似於C語言中的斷言,但還是有區別:一般在Release版本中,斷言被定義為空而失效,但需要有if校驗存在進行異常保護,儘管契約式設計中不建議這樣做。在Golang中,recover完全可以終止異常展開過程,省時省力。
我們在呼叫recover的延遲函式中以最合理的方式響應該異常:
- 列印堆疊的異常呼叫資訊和關鍵的業務資訊,以便這些問題保留可見;
- 將異常轉換為錯誤,以便呼叫者讓程式恢復到健康狀態並繼續安全執行。
我們看一個簡單的例子:
func funcA() error {
defer func() {
if p := recover(); p != nil {
fmt.Printf("panic recover! p: %v", p)
debug.PrintStack()
}
}()
return funcB()
}
func funcB() error {
// simulation
panic("foo")
return errors.New("success")
}
func test() {
err := funcA()
if err == nil {
fmt.Printf("err is nil\\n")
} else {
fmt.Printf("err is %v\\n", err)
}
}
我們期望test函式的輸出是:
err is foo
實際上test函式的輸出是:
err is nil
原因是panic異常處理機制不會自動將錯誤資訊傳遞給error,所以要在funcA函式中進行顯式的傳遞,程式碼如下所示:
func funcA() (err error) {
defer func() {
if p := recover(); p != nil {
fmt.Println("panic recover! p:", p)
str, ok := p.(string)
if ok {
err = errors.New(str)
} else {
err = errors.New("panic")
}
debug.PrintStack()
}
}()
return funcB()
}
姿勢三:對於不應該出現的分支,使用異常處理
當某些不應該發生的場景發生時,我們就應該呼叫panic函式來觸發異常。比如,當程式到達了某條邏輯上不可能到達的路徑:
switch s := suit(drawCard()); s {
case "Spades":
// ...
case "Hearts":
// ...
case "Diamonds":
// ...
case "Clubs":
// ...
default:
panic(fmt.Sprintf("invalid suit %v", s))
}
姿勢四:針對入參不應該有問題的函式,使用panic設計
入參不應該有問題一般指的是硬編碼,我們先看這兩個函式(Compile和MustCompile),其中MustCompile函式是對Compile函式的包裝:
func MustCompile(str string) *Regexp {
regexp, error := Compile(str)
if error != nil {
panic(`regexp: Compile(` + quote(str) + `): ` + error.Error())
}
return regexp
}
所以,對於同時支援使用者輸入場景和硬編碼場景的情況,一般支援硬編碼場景的函式是對支援使用者輸入場景函式的包裝。
對於只支援硬編碼單一場景的情況,函式設計時直接使用panic,即返回值型別列表中不會有error,這使得函式的呼叫處理非常方便(沒有了乏味的"if err != nil {/ 列印 && 錯誤處理 /}"程式碼塊)。