美思 [Golang] 程式設計教學:錯誤處理 (Error Handling)

Golang錯誤處理
Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Email

前言

在實際情境中運行的程式,即使程式本身沒有臭蟲 (bug),仍然要面對許多可能的錯誤 (error)。例如,想要將某個字串轉成數字,但字串本身不是合法的數字;想要讀取某個外部檔案,卻權限不足;想要解析某個 XML 檔案,但該 XML 檔案內有錯誤的格式。我們不能天真地認定程式不會發生錯誤,而要詳細考慮可能發生的錯誤,撰寫相關的程式碼。

處理錯誤的策略

Go 程式對錯誤的處理有兩大類方式:

  • 一般錯誤:拋出錯誤物件,交由後續的程式自行處理
  • 嚴重錯誤:引發 panic 事件,將程式提早結束

要採用那一種方式沒有制式的規定,完全依賴套件設計者的權衡考量。

在 Go 程式中,除以零是一種嚴重錯誤。參考以下程式碼:

package main                 /* 1 */

import (                     /* 2 */
        "fmt"                /* 3 */
)                            /* 4 */

func main() {                /* 5 */
        fmt.Println(3 / 0)   /* 6 */
}                            /* 7 */

在第 6 行中,我們試圖計算 3 / 0,Golang 將其視為嚴重錯誤,故強制中止程式。

然而,開啟檔案時引發錯誤,不會直接讓程式當掉,而會由使用者決定下一步要如何處理:

package main                                     /*  1 */

import "os"                                      /*  2 */

func main() {                                    /*  3 */
        file, err := os.Open("file.txt")         /*  4 */
        if err != nil {                          /*  5 */
                /* Handle the error. */          /*  6 */
        }                                        /*  7 */
        defer file.Close()                       /*  8 */

        /* Do something on the file object. */   /*  9 */
}                                                /* 10 */

在第 4 行中,我們試圖開啟 file.txt 。這是一個有可能失敗的動作,所以,除了回傳 file 物件外,os.Open() 函式還多回傳了 err 物件。

在第 5 行至第 7 行中,我們會檢查 err 物件是否為空。當 err 物件不為空,代表存取 file.txt 時發生某種錯誤。這時候我們終止一般的流程,改進行錯誤處理的流程。

由於要從嚴重錯誤中回復較困難,對大部分的情境來說,使用一般錯誤是較佳的的選擇。

生成和使用一般錯誤

在 Go 裡面,沒有 throw ,也沒有 trycatchfinally 這種針對錯誤處理的特殊區塊,而是使用錯誤物件來代表錯誤事件發生。錯誤物件也是一個值。該物件不會將程式強迫中止,而由使用者決定下一步要怎麼做。

參考以下例子:

package main                              /*  1 */

import (                                  /*  2 */
        "errors"                          /*  3 */
        "fmt"                             /*  4 */
)                                         /*  5 */

func main() {                             /*  6 */
        err := errors.New("Some error")   /*  7 */
        if err != nil {                   /*  8 */
                fmt.Println(err)          /*  9 */
        }                                 /* 10 */

        fmt.Println("More message")       /* 11 */
}                                         /* 12 */

在第 7 行時,我們生成錯誤物件 err。由於 err 不為空,在第 9 行會印出 err 內的訊息。

但程式沒有提早結束,程式會繼續行進。所以第 11 行的一般訊息也會印出來。

處理一般錯誤的模式

在 Go 程式中,錯誤物件通常會和其他值一併回傳,由套件使用者自行檢查及決定下一個步驟。如下例:

package main                              /*  1 */

import (                                  /*  2 */
        "fmt"                             /*  3 */
        "log"                             /*  4 */
        "strconv"                         /*  5 */
)                                         /*  6 */

func main() {                             /*  7 */
        n, err := strconv.Atoi("hello")   /*  8 */
        if err != nil {                   /*  9 */
                log.Fatal(err)            /* 10 */
        }                                 /* 11 */

        fmt.Println(n)                    /* 12 */
}                                         /* 13 */

在第 8 行中,strconv.Atoi() 函式回傳了兩個值,分別是轉換後的值和錯誤物件。該函式本身不處理錯誤物件,委由外部程式來決定如何處理錯誤情境。這是典型的一般錯誤的處理模式。

如果我們很確定程式不會有錯誤,我們也可以忽略錯誤物件。如下例:

package main                          /*  1 */

import (                              /*  2 */
        "fmt"                         /*  3 */
        "strconv"                     /*  4 */
)                                     /*  5 */

func main() {                         /*  6 */
        n, _ := strconv.Atoi("123")   /*  7 */

        fmt.Println(n)                /*  8 */
}                                     /*  9 */

在第 7 行中,我們不想取得錯誤物件,故用啞變數 _ 來忽略回傳值。

另外一種錯誤處理的變化形態是回傳布林值,例如,我們在檢查 map 的鍵/值對時。見下例:

package main

import (
    "log"
)

func main() {
    m := map[int]string{
        1: "one",
        2: "two",
        3: "three",
    }

    v, ok := m[1]
    if !ok {
        log.Fatal("Unable to retrieve key/value pair")
    }

    if !(v == "one") {
        log.Fatal("Wrong value")
    }
}

錯誤介面

如果想實作自己的錯誤物件,Go 提供一個公開介面,只要符合該介面的要求即可。

type error interface {
    Error() string
}

嚴重錯誤

如果想引發嚴重錯誤,使用 panic 函式即可。見下例:

package main

import (
    "fmt"
)

func main() {
    panic("Some error")

    // It didn't occur.
    fmt.Println("More message")
}

在這個例子中,程式遇到 panic 所在的地方即中止程式,不會執行後續的指令。

從嚴重錯誤中回復

如果想要從嚴重錯誤中回復,要用 recover 函式,並且要搭配特定的語法。見下例:

package main

import (
    "fmt"
)

func main() {
    defer func() {
        err := recover()
        if err != nil {
            fmt.Println("Recover from panic")
        }
    }()

    panic("Some error")

    // It didn't occur.
    fmt.Println("More message")
}

濫用 recover 會使得程式可讀性較差,不會將其常態性使用。

關於作者

身為資訊領域碩士,美思認為開發應用程式的目的是為社會帶來價值。如果在這個過程中該軟體能成為永續經營的項目,那就是開發者和使用者雙贏的局面。

美思喜歡用開源技術來解決各式各樣的問題,但必要時對專有技術也不排斥。閒暇之餘,美思將所學寫成文章,放在這個網站上和大家分享。