Go 經典入門系列 30:錯誤處理

2021-02-14 Go語言中文網

歡迎來到 Golang 系列教程[1]的第 30 篇。

什麼是錯誤?

錯誤表示程序中出現了異常情況。比如當我們試圖打開一個文件時,文件系統裡卻並沒有這個文件。這就是異常情況,它用一個錯誤來表示。

在 Go 中,錯誤一直是很常見的。錯誤用內建的 error 類型來表示。

就像其他的內建類型(如 int、float64 等),錯誤值可以存儲在變量裡、作為函數的返回值等等。

示例

現在我們開始編寫一個示例,該程序試圖打開一個並不存在的文件。

package main

import (
    "fmt"
    "os"
)

func main() {
    f, err := os.Open("/test.txt")
    if err != nil {
        fmt.Println(err)
        return
    }
    fmt.Println(f.Name(), "opened successfully")
}

在 playground 中運行[2]

在程序的第 9 行,我們試圖打開路徑為 /test.txt 的文件(playground 顯然並不存在這個文件)。os 包裡的 `Open`[3] 函數有如下簽名:

func Open(name string) (file *File, err error)

如果成功打開文件,Open 函數會返回一個文件句柄(File Handler)和一個值為 nil 的錯誤。而如果打開文件時發生了錯誤,會返回一個不等於 nil 的錯誤

如果一個函數[4] 或方法[5] 返回了錯誤,按照慣例,錯誤會作為最後一個值返回。於是 Open 函數也是將 err 作為最後一個返回值。

按照 Go 的慣例,在處理錯誤時,通常都是將返回的錯誤與 nil 比較。nil 值表示了沒有錯誤發生,而非 nil 值表示出現了錯誤。在這裡,我們第 10 行檢查了錯誤值是否為 nil。如果不是 nil,我們會簡單地列印出錯誤,並在 main 函數中返回。

運行該程序會輸出:

open /test.txt: No such file or directory

很棒!我們得到了一個錯誤,它指出該文件並不存在。

錯誤類型的表示

讓我們進一步深入,理解 error 類型是如何定義的。error 是一個接口[6]類型,定義如下:

type error interface {
    Error() string
}

error 有了一個籤名為 Error() string 的方法。所有實現該接口的類型都可以當作一個錯誤類型。Error() 方法給出了錯誤的描述。

fmt.Println 在列印錯誤時,會在內部調用 Error() string 方法來得到該錯誤的描述。上一節示例中的第 11 行,就是這樣列印出錯誤的描述的。

從錯誤獲取更多信息的不同方法

現在,我們知道了 error 是一個接口類型,讓我們看看如何從一個錯誤獲取更多信息。

在前面的示例裡,我們只是列印出錯誤的描述。如果我們想知道這個錯誤的文件路徑,該怎麼做呢?一種選擇是直接解析錯誤的字符串。這是前面示例的輸出:

open /test.txt: No such file or directory

我們解析了這條錯誤信息,雖然獲取了發生錯誤的文件路徑,但是這種方法很不優雅。隨著語言版本的更新,這條錯誤的描述隨時都有可能變化,使我們程序出錯

有沒有更加可靠的方法來獲取文件名呢?答案是肯定的,這是可以做到的,Go 標準庫給出了各種提取錯誤相關信息的方法。我們一個個來看看吧。

1. 斷言底層結構體類型,使用結構體欄位獲取更多信息

如果你仔細閱讀了 `Open`[7] 函數的文檔,你可以看見它返回的錯誤類型是 *PathError。`PathError`[8] 是結構體[9]類型,它在標準庫中的實現如下:

type PathError struct {
    Op   string
    Path string
    Err  error
}

func (e *PathError) Error() string { return e.Op + " " + e.Path + ": " + e.Err.Error() }

如果你有興趣了解上述原始碼出現的位置,可以在這裡找到:https://golang.org/src/os/error.go?s=653:716#L11。

通過上面的代碼,你就知道了 *PathError 通過聲明 Error() string 方法,實現了 error 接口。Error() string 將文件操作、路徑和實際錯誤拼接,並返回該字符串。於是我們得到該錯誤信息:

open /test.txt: No such file or directory

結構體 PathError 的 Path 欄位,就有導致錯誤的文件路徑。我們修改前面寫的程序,列印出該路徑。

package main

import (
    "fmt"
    "os"
)

func main() {
    f, err := os.Open("/test.txt")
    if err, ok := err.(*os.PathError); ok {
        fmt.Println("File at path", err.Path, "failed to open")
        return
    }
    fmt.Println(f.Name(), "opened successfully")
}

在 playground 上運行[10]

在上面的程序裡,我們在第 10 行使用了類型斷言[11](Type Assertion)來獲取 error 接口的底層值(Underlying Value)。接下來在第 11 行,我們使用 err.Path 來列印該路徑。該程序會輸出:

File at path /test.txt failed to open

很棒!我們已經使用類型斷言成功獲取到了該錯誤的文件路徑。

2. 斷言底層結構體類型,調用方法獲取更多信息

第二種獲取更多錯誤信息的方法,也是對底層類型進行斷言,然後通過調用該結構體類型的方法,來獲取更多的信息。

我們通過一個實例來理解這一點。

標準庫中的 DNSError 結構體類型定義如下:

type DNSError struct {
    ...
}

func (e *DNSError) Error() string {
    ...
}
func (e *DNSError) Timeout() bool {
    ...
}
func (e *DNSError) Temporary() bool {
    ...
}

從上述代碼可以看到,DNSError 結構體還有 Timeout() bool 和 Temporary() bool 兩個方法,它們返回一個布爾值,指出該錯誤是由超時引起的,還是臨時性錯誤。

接下來我們編寫一個程序,斷言 *DNSError 類型,並調用這些方法來確定該錯誤是臨時性錯誤,還是由超時導致的。

package main

import (
    "fmt"
    "net"
)

func main() {
    addr, err := net.LookupHost("golangbot123.com")
    if err, ok := err.(*net.DNSError); ok {
        if err.Timeout() {
            fmt.Println("operation timed out")
        } else if err.Temporary() {
            fmt.Println("temporary error")
        } else {
            fmt.Println("generic error: ", err)
        }
        return
    }
    fmt.Println(addr)
}

註:在 playground 無法進行 DNS 解析。請在你的本地運行該程序

在上述程序中,我們在第 9 行,試圖獲取 golangbot123.com(無效的域名) 的 ip。在第 10 行,我們通過 *net.DNSError 的類型斷言,獲取到了錯誤的底層值。接下來的第 11 行和第 13 行,我們分別檢查了該錯誤是由超時引起的,還是一個臨時性錯誤。

在本例中,我們的錯誤既不是臨時性錯誤,也不是由超時引起的,因此該程序輸出:

generic error:  lookup golangbot123.com: no such host

如果該錯誤是臨時性錯誤,或是由超時引發的,那麼對應的 if 語句會執行,於是我們就可以適當地處理它們。

3. 直接比較

第三種獲取錯誤的更多信息的方式,是與 error 類型的變量直接比較。我們通過一個示例來理解。

filepath 包中的 `Glob`[12] 用於返回滿足 glob 模式的所有文件名。如果模式寫的不對,該函數會返回一個錯誤 ErrBadPattern。

filepath 包中的 ErrBadPattern 定義如下:

var ErrBadPattern = errors.New("syntax error in pattern")

errors.New() 用於創建一個新的錯誤。我們會在下一教程中詳細討論它。

當模式不正確時,Glob 函數會返回 ErrBadPattern。

我們來寫一個小程序來看看這個錯誤。

package main

import (
    "fmt"
    "path/filepath"
)

func main() {
    files, error := filepath.Glob("[")
    if error != nil && error == filepath.ErrBadPattern {
        fmt.Println(error)
        return
    }
    fmt.Println("matched files", files)
}

在 playground 上運行[13]

在上述程序裡,我們查詢了模式為 [ 的文件,然而這個模式寫的不正確。我們檢查了該錯誤是否為 nil。為了獲取該錯誤的更多信息,我們在第 10 行將 error 直接與 filepath.ErrBadPattern 相比較。如果該條件滿足,那麼該錯誤就是由模式錯誤導致的。該程序會輸出:

syntax error in pattern

標準庫在提供錯誤的詳細信息時,使用到了上述提到的三種方法。在下一教程裡,我們會通過這些方法來創建我們自己的自定義錯誤。

不可忽略錯誤

絕不要忽略錯誤。忽視錯誤會帶來問題。接下來我重寫上面的示例,在列出所有滿足模式的文件名時,我省略了錯誤處理的代碼。

package main

import (
    "fmt"
    "path/filepath"
)

func main() {
    files, _ := filepath.Glob("[")
    fmt.Println("matched files", files)
}

在 playground 上運行[14]

我們已經從前面的示例知道了這個模式是錯誤的。在第 9 行,通過使用 _ 空白標識符,我忽略了 Glob 函數返回的錯誤。我在第 10 行簡單列印了所有匹配的文件。該程序會輸出:

matched files []

由於我忽略了錯誤,輸出看起來就像是沒有任何匹配了 glob 模式的文件,但實際上這是因為模式的寫法不對。所以絕不要忽略錯誤。

本教程到此結束。

這一教程我們討論了該如何處理程序中出現的錯誤,也討論了如何查詢關於錯誤的更多信息。簡單概括一下本教程討論的內容:

在下一教程,我們會創建我們自己的自定義錯誤,並給標準錯誤增加更多的語境(Context)。

祝你愉快。

上一教程 - Defer

下一教程 - 自定義錯誤

via: https://golangbot.com/error-handling/

作者:Nick Coghlan[15]譯者:Noluye[16]校對:polaris1119[17]

本文由 GCTT[18] 原創編譯,Go 中文網[19] 榮譽推出

參考資料[1]

Golang 系列教程: https://studygolang.com/subject/2

[2]

在 playground 中運行: https://play.golang.org/p/yOhAviFM05

[3]

Open: https://golang.org/pkg/os/#Open

[4]

函數: https://studygolang.com/articles/11892

[5]

方法: https://studygolang.com/articles/12264

[6]

接口: https://studygolang.com/articles/12266

[7]

Open: https://golang.org/pkg/os/#OpenFile

[8]

PathError: https://golang.org/pkg/os/#PathError

[9]

結構體: https://studygolang.com/articles/12263

[10]

在 playground 上運行: https://play.golang.org/p/JQrqWU7Jf9

[11]

類型斷言: https://studygolang.com/articles/12266

[12]

Glob: https://golang.org/pkg/path/filepath/#Glob

[13]

在 playground 上運行: https://play.golang.org/p/zbVDDHnMZU

[14]

在 playground 上運行: https://play.golang.org/p/2k8r_Qg_lc

[15]

Nick Coghlan: https://golangbot.com/about/

[16]

Noluye: https://github.com/Noluye

[17]

polaris1119: https://github.com/polaris1119

[18]

GCTT: https://github.com/studygolang/GCTT

[19]

Go 中文網: https://studygolang.com/

相關焦點

  • Go 經典入門系列 28:Defer
    歡迎來到 Golang 系列教程[1]的第 29 篇。什麼是 defer? defer 語句的用途是:含有 defer 語句的函數,會在該函數將要返回之前,調用另一個函數。這個定義可能看起來很複雜,我們通過一個示例就很容易明白了。
  • Go 經典入門系列 32:panic 和 recover
    panic 和 recover歡迎來到 Golang 系列教程[1]的第 32 篇。什麼是 panic? 在 Go 語言中,程序中一般是使用錯誤[2]來處理異常情況。對於程序中出現的大部分異常情況,錯誤就已經夠用了。
  • Go 經典入門系列 26:結構體取代類​
    歡迎來到 Golang 系列教程[1]的第 26 篇。Go 支持面向對象嗎? Go 並不是完全面向對象的程式語言。Go 官網的 FAQ[2] 回答了 Go 是否是面向對象語言,摘錄如下。可以說是,也可以說不是。
  • Go 經典入門系列 24:Select
    歡迎來到 Golang 系列教程[1]的第 24 篇。什麼是 select? select 語句用於在多個發送/接收信道操作中進行選擇。select 語句會一直阻塞,直到發送/接收操作準備就緒。如果有多個信道操作準備完畢,select 會隨機地選取其中之一執行。
  • [GO語言基礎] 一.為什麼我要學習Golang以及GO語言入門普及
    這系列文章入門部分將參考「尚矽谷」韓順平老師的視頻和書籍《GO高級編程》,詳見參考文獻,並結合作者多年的編程經驗進行學習和豐富,且看且珍惜吧!後續會結合網絡安全進行GO語言實戰深入,加油~這些年我學過各種程式語言,從最早的C語言到C++,再到VB、C#、PHP、JAVA,再到IOS開發、Python,到最新的GO語言,學得是真的雜。
  • Go 經典入門系列 10:switch 語句
    這是 Golang 系列教程 中的第 10 篇。switch 是一個條件語句,用於將表達式的值與可能匹配的選項列表進行比較,並根據匹配情況執行相應的代碼塊。它可以被認為是替代多個 if else 子句的常用方式。
  • 大牌經典30款入門必選耳飾
    今天挖寶給大家帶來的是大牌經典的30款入門必選耳飾單品!~髮絲撩動間,美麗盡顯,絕對的時尚單品,增添魅力和氣質。集美們速來收藏!!~~Van Cleef & Arpels梵克雅寶——Alhambra耳釘系列精美四葉草 愛心造型,精緻又百搭 入手沒錯~~Tiffany蒂芙尼——Tiffany T 耳釘系列大牌感十足,詮釋自我風格的強大力量,新品讓你更加凸顯時尚;
  • 用Python數據處理分析入門必備系列文章:基本類型與運算
    此系列文章收錄在公眾號 : 數據大宇宙 > Python入門必備 > 必備知識建議你按其順序觀看文章最近有許多小夥伴問我要入門 Python 的資料,還有小夥伴完全沒有入門 Python 就直接購買了我的 pandas 專欄。因此我決定寫幾篇 Python 數據處理分析必備的入門知識系列文章,以幫助有需要的小夥伴們更好入門。
  • Go語言錯誤總結(一)
    錯誤代碼:package mainimport "fmt"func main() { fmt.Println("hello world!")}編譯錯誤:./main.go:5:6: missing function body for "main".
  • 入門教程:花 5 分鐘學習 Go 語言
    原文如下:基本作為經典的 「Hello World」,你的第一個 Go 程序非常簡單:先為我們的項目創建 workspace:$ mkdir hello$ cd hello接著創建並初始化 Go module:$ go mod init hello使用你最喜歡的編輯器創建
  • GO語言入門(第一個go程序)
    本文節選自《go入門指南》GO語言介紹指導設計原則
  • MATLAB入門教程系列--圖形處理
    前面推送了兩篇MATLAB入門教程系列文章,今天是這個系列文章的最後一篇,我們一起看看MATLAB強大的圖形處理能力。
  • 入門:初學ASP動態網頁製作常用錯誤處理
    首頁 > 語言 > 關鍵詞 > 網頁最新資訊 > 正文 入門:初學ASP動態網頁製作常用錯誤處理
  • Go語言常見語法錯誤
    }編譯錯誤:./main.go:5:6: missing function body for "main".錯誤代碼:package mainimport (    "fmt"    "log"    "time")func main() {}編譯錯誤:./main.go:4:2: imported and not used: "fmt".
  • 經典巧板系列:七巧板入門基礎問題004
    平面幾何是小學到初中階段都要學習的重要知識,七巧板則是經典的中國傳統智力遊戲,玩好七巧板有助於孩子學習平面幾何知識,對於培養數學的思維有也一定的幫助。從今天(9月16日)開始,我們推出《經典巧板系列:七巧板入門基礎問題》,該系列共40期,每期1-3個與七巧板相關的基礎問題,所有問題由益智遊戲方面的專家武元元老師提供。
  • 經典巧板系列:七巧板入門基礎問題003
    平面幾何是小學到初中階段都要學習的重要知識,七巧板則是經典的中國傳統智力遊戲,玩好七巧板有助於孩子學習平面幾何知識,對於培養數學的思維有也一定的幫助。從今天(9月16日)開始,我們推出《經典巧板系列:七巧板入門基礎問題》,該系列共40期,每期1-3個與七巧板相關的基礎問題,所有問題由益智遊戲方面的專家武元元老師提供。
  • 經典巧板系列:七巧板入門基礎問題002
    平面幾何是小學到初中階段都要學習的重要知識,七巧板則是經典的中國傳統智力遊戲,玩好七巧板有助於孩子學習平面幾何知識,對於培養數學的思維有也一定的幫助。從今天(9月16日)開始,我們推出《經典巧板系列:七巧板入門基礎問題》,該系列共40期,每期1-3個與七巧板相關的基礎問題,所有問題由益智遊戲方面的專家武元元老師提供。
  • JavaScript錯誤處理完全指南
    出現致命的錯誤時可能就會是這種情況,因為停止程序比處理無效數據更安全。介紹了基礎知識之後,現在我們來研究 同步和異步 JavaScript 代碼中的錯誤和異常處理。同步代碼在大多數情況下很簡單,它的錯誤處理也是如此。同步代碼的執行順序和代碼的編寫順序一致。
  • Go 快速入門篇(三):單元測試、問題定位及代碼調試
    單元測試文件默認以同一目錄下文件名後綴 _test 作為標識,比如我們在 simplemath 目錄下新建 add_test.go 和 sqrt_test.go 文件,分別為 add.go 和 sqrt.go 編寫單元測試,對應的目錄結構如下:
  • JavaScript 錯誤處理大全【建議收藏】
    同步錯誤處理異步錯誤處理Node.js中的錯誤處理Node.js 中的同步錯誤處理Node.js 中的異步錯誤處理:回調模式Node.js 中的異步錯誤處理:事件發射器總結❞編程中有什麼錯誤?當發生致命的錯誤,需要更安全地停止程序而不是處理無效數據時,你可能需要這樣做。介紹了基礎知識之後,現在讓我們將注意力轉向同步和異步 JavaScript 代碼中的錯誤和異常處理。同步錯誤處理同步代碼通常很簡單,它的錯誤處理也是如此。