【GoLang 那點事】深入 Go 的錯誤處理機制 (一) 使用

a_wei發表於2019-08-14

開篇詞

程式執行過程中不可避免的發生各種錯誤,要想讓自己的程式保持較高的健壯性,那麼異常,錯誤處理是需要考慮周全的,每個程式語言提供了一套自己的異常錯誤處理機制,在Go中,你知道了嗎?接下來我們一起看看Go的異常錯誤機制。

Go錯誤處理,函式多返回值是前提

  • 首先我們得明確一點,Go是支援多返回值的,如下,sum函式進行兩個int型資料的求和處理,函式結果返回最終的和(z)以及入參(x,y),既然支援多返回值,同理,我們能否把錯誤資訊返回呢?當然是可以的
func sum (x,y int) (int,int,int){
    z := x+y
    return z,x,y
}

Go內建的錯誤型別

  • 在Go中內建了一個error介面用來用來處理錯誤
// The error built-in interface type is the conventional interface for
// representing an error condition, with the nil value representing no error.
//翻譯下來就是:
//錯誤的內建介面型別是 error,沒有錯誤用 nil 表示
type error interface {  
    Error() string
}
  • 我們來看Go內建的一個關於error介面的簡單實現
func New(text string) error {        
    return &errorString{text}
}
// errorString is a trivial implementation of error.
翻譯
// 把error轉換成String是錯誤的簡單實現
type errorString struct {        
    s string
}
func (e *errorString) Error() string { 
    return e.s
}

我們可以看到errorString結構體實現了 Error()string 介面,通過New()方法返回一個errorString指標型別的物件。

看到這裡不知道大家想到沒,Go對錯誤的處理就是顯示的通過方法返回值告訴你需要對錯誤進行判斷和處理。也就是錯誤對你是可見的,這也需要開發人員在方法中儘可能的考慮到各種發生的錯誤,並返回給方法呼叫者。

Go內建的異常捕獲

我們知道程式在執行時會發生各種各樣的執行時錯誤,比如陣列下標越界異常,除數為0的異常等等,而這些異常如果不被處理會導致go程式的崩潰,那麼如何捕獲這些執行時異常轉化為錯誤返回給上層呼叫鏈,就讓我一起看看這幾個關鍵字:panic, defer recover,此處我們不討論原理。

  • go內建了這幾個關鍵字,下面是這幾個關鍵字的含義:

    1. panic 恐慌
    2. defer 推遲,延緩
    3. recover 恢復
  • 我們把執行時發生異常稱為發生了一個恐慌,我們也可以手動丟擲一個恐慌,如下程式碼

func TestPanic(){
    panic("發生恐慌了")
}
//擷取一部分結果,我們看到程式終止了,列印了堆疊資訊
anic: 發生恐慌了 [recovered]
    panic: 發生恐慌了
goroutine 19 [running]:
testing.tRunner.func1(0xc0000b6100)
    D:/sdk/go12/src/testing/testing.go:830 +0x399
panic(0x521da0, 0x57bb10)
    D:/sdk/go12/src/runtime/panic.go:522 +0x1c3
gome_tools/basic.TestPanic(...)
    D:/gome_space/gome_tools/basic/array_slice.go:101

恐慌發生了怎麼處理呢,這時需要defer和recover一起協作,defer什麼意思呢,是表示這個方法最後執行的一段程式碼,無論這個方法發生錯誤,異常等,defer裡面的裡程式碼一定會被執行,而我們可以在defer中通過recover關鍵字恢復我們的恐慌,將之處理,轉化為一個錯誤並列印,如下程式碼:

func TestDeferAndRecover(){
    defer func(){
        if err:=recover(); err != nil{
            fmt.Println("異常資訊為:",err)
        }
    }()    
    panic("發生恐慌了")
}
//結果
異常資訊為: 發生恐慌了

Go異常錯誤處理示例

  • 接下來我們看一個除法函式,函式中,如果被除數為0,則除法運算是失敗的
func division(x,y int) (int,error){
    //如果除數為0,則返回一個錯誤資訊給上游
    if y == 0{
        return 0,errors.New("y is not zero")
    }   
    z := x / y
    return z ,nil
}

result, err := division(1,0)
if err != nil {
    //處理錯誤邏輯
}
//處理正常邏輯

如上,division函式裡面判斷y等於0時,給呼叫者返回一個錯誤資訊,呼叫者通過兩個變數來接受division的返回值,判斷 err是否為空做出不同的錯誤處理邏輯

  • 有些錯誤,我們知道是不可能發生的,那麼如何忽略這類錯誤呢?

還是上面的 division(x,y)(z,error) 函式,假設我們入參傳(4,2)進去,這時我們是清楚的知道不可能發生錯誤,我們可以按如下處理,通過下劃線 _ 忽略這個返回值。

//通過_忽略第二個返回值
result, _ := division(1,0)
//列印結果
fmt.Println(result)
  • 只要是人,總有考慮不周的時候,總有想不到的時候,

還是division(x,y)(z,error)函式,假設小明忘記了或者沒想到要判斷除數為0的情況,寫出來的程式碼如下:

func division(x,y int) (int,error){
    z := x / y
    return z ,nil
}

小紅在呼叫上面的方法時寫成了 result,_ := division(1,0),很明顯division方法是會發生錯誤的,錯誤資訊如下,integer divide by zero ,被除數為0,我們知道程式出錯了,並且整個程式終止了

tips: Go語言中,一旦某一個協程發生了panic而沒有被捕獲,那麼導致整個go程式都會終止,確實有點坑,但確實如此(瞭解java的人都知道,在java中一個執行緒發生發生了異常,只要其主執行緒不曾終止,那麼整個程式還是執行的) ,但go不是這樣的,文章最後我會寫一個例子,大家可以看看。

通過上面的tips,我們知道,我們不能讓我們的方法發生panic,在不確保方法不會發生panic時一定要捕獲,謹記。

panic: runtime error: integer divide by zero [recovered]
    panic: runtime error: integer divide by zero
  • 發生panic時,如何捕獲這個panic給上游返回一個error
func division(x,y int) (result int,err error){
    defer func(){
        if e := recover(); e != nil{
            err = e.(error)
        }
    }()
    result = x / y
    return result ,nil
}

這段程式碼什麼意思呢?當我們division(1,0)時,一定會報除0異常,division函式宣告瞭返回值result(int型),err(error型),當x / y發生異常時,在defer函式中,我們通過recover()函式來捕獲發生的異常,如果不為空,將這個異常賦值給返回結果的變數 err,我們再來呼叫這個函式division(1,0)看看輸出什麼,如下,是不是將堆疊資訊轉化為了一段字串描述。

0 runtime error: integer divide by zero
  • 如何自定義自己的錯誤型別

  • 我們知道go中關於錯誤定義了一個介面,如果想要自定義自己的錯誤型別,我們只需要實現這個介面就可以了,還是這個函式,我們為其定義一個除數為0的錯誤

type DivideByZero struct{
    //錯誤資訊
    e string
    //入參資訊(除數和被除數)
    param string
}
//實現介面中的Error()string方法,組裝錯誤資訊為字串
func (e *DivideByZero) Error() string { 
    buffer := bytes.Buffer{}
    buffer.WriteString("錯誤資訊:")
    buffer.WriteString(divideByZero.e)
    buffer.WriteString(",入參資訊:")
    buffer.WriteString(divideByZero.param)
    return buffer.String()
}
func division(x,y int) (int,error){
    //如果除數為0,則返回一個錯誤資訊給上游
    if y == 0{
        //這個時候我們返回如下錯誤
        return 0, &DivideByZero{
            e:"除數不能0",
            param:strings.Join([]string{strconv.Itoa(x),strconv.Itoa(y)},","),
        }
    }   
    z := x / y
    return z ,nil
}
//最終結果
0 錯誤資訊:除數不能為0,入參資訊:1,0

最後補一下上面說的示例

  • 上文提到,go中一旦某一個協程發生了panic而沒被recover,那麼整個go程式會終止,而Java中,某一執行緒發生了異常,即便沒被catche,那麼只是這個執行緒終止了,Java程式是不會終止的,只有主執行緒完成Java程式才會結束,看下面兩段程式碼
public static void main(String []args){
    new Thread(new Runnable() {    
        @Override    
        public void run() {
            throw new RuntimeException("丟擲異常了");   
        }
    }).start();
    try {    
        Thread.sleep(10 * 1000);
    }catch (InterruptedException e) {
    }
}
func main(){
    go func() {
        panic("發生恐慌了")
    }()
    time.Sleep(10 * time.Second)
}

上面兩端程式碼含義都是一樣的,啟動後各開一個執行緒和協程,線上程和協程內分別主動丟擲異常,但結果不一樣,java的主執行緒會休眠10秒鐘後結束,而go主協程會立即結束。

歡迎大家關注微信公眾號:“golang那點事”,更多精彩期待你的到來

【GoLang那點事】深入Go的錯誤處理機制(一)使用

那小子阿偉

相關文章