深入Go的錯誤處理機制使用

開篇詞

程序運行過程中不可避免的發生各種錯誤,要想讓自己的程序保持較高的健壯性,那麼異常,錯誤處理是需要考慮周全的,每個編程語言提供了一套自己的異常錯誤處理機制,在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主協程會立即結束。

歡迎大家關注微信公衆號:技術人技術事,更多精彩期待你的到來

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章