錯誤處理(包括日誌記錄)

錯誤處理

簡單的錯誤處理是使用 Fprintf 和 %v 在標準錯誤流上輸出一條消息,%v 可使用默認格式顯示任意類型的值。
爲了保持示例代碼簡短,有時會對錯誤處理有意進行必定程度的忽略。明顯的錯誤仍是要處理的。可是有些出現機率很小的錯誤,就忽略了,不過要標記所跳過的錯誤檢查,就是加上註釋。 html

根據情形,將有許多可能的處理場景,接下來是5個例子。 安全

1、將錯誤傳遞下去

最多見的情形是將錯誤傳遞下去,使得在子例程中發生的錯誤變爲主調例程的錯誤。
一種是不作任何操做當即向調用者返回錯誤:服務器

resp, err := http.Get(url)
if err != nil {
    return nil, err
}

還有一種,不會直接返回,由於錯誤信息中缺失一些關鍵信息:ide

doc, err := html.Parse(resp.Body)
resp.Body.Close()
if err != nil {
    return nil, fmt.Errorf("parsing %s as HTML: %v\n", url, err)
}

這裏格式化了一條錯誤消息而且返回一個新的錯誤值。能夠爲原始的錯誤消息不斷地添加上下文信息來創建一個可讀的錯誤描述。當錯誤最終被程序的 main 函數處理時,它應該可以提供一個從最根本問題到整體故障的清晰因果鏈、這裏有一個 NASA 的事故調查的例子:函數

genesis: crashed: no parachute: G-switch failed: bad relay orientation

由於錯誤頻繁地串聯起來,因此消息字符串首字母不該該大寫並且應該避免換行。錯誤結果可能會很長,但能可以使用 grep 這樣的工具找到須要的信息。 工具

須要添加的關鍵信息
有時候能夠不用添加信息直接返回,有時候須要添加一些關鍵信息,由於錯誤信息裏沒有。好比 os.Open 打開文件時,返回的錯誤不只僅包括錯誤的信息,還包含文件的名字,所以調用者構造錯誤消息的時候不須要包含文件的名字這類信息。具體哪些信息是缺乏的關鍵信息須要在原始的錯誤消息的基礎上添加?
通常地,f(x) 調用只負責報告函數的行爲 f 和參數值 x,由於它們和錯誤的上下文相關。調用者則負責添加進一步的信息,可是 f(x) 自己並不會,而且在函數內部也沒有這些信息。
好比上面的 html.Parse 返回的錯誤信息裏不可能有 url 的信息,可是,是關鍵信息須要添加。而 os.Open 中,文件名字也是關鍵信息,可是這個正是函數的參數值,因此函數自己會返回這個信息,不須要另外添加。 ui

2、嘗試重試

對於不固定或者不可預測的錯誤,在短暫的間隔後對操做進行重試是合乎情理的。超出必定的重試次數和限定的時間後再報錯退出。
下面給出了完整的代碼,暫時只看 WaitForServer 函數:url

package main

import (
    "fmt"
    "log"
    "net/http"
    "os"
    "time"
)

// 嘗試鏈接 url 對應的服務器
// 在一分鐘內使用指數退避策略進行重試
// 全部的嘗試失敗後返回錯誤
func WaitForServer(url string) error {
    const timeout = 1 * time.Minute
    deadline := time.Now().Add(timeout)
    for tries := 0; time.Now().Before(deadline); tries++ {
        _, err := http.Head(url)
        if err == nil {
            return nil // 成功
        }
        log.Printf("server not responding (%s); retrying...", err)
        time.Sleep(time.Second << uint(tries)) // 指數退避策略
    }
    return fmt.Errorf("server %s failed to respond after %s", url, timeout)
}

func main() {
    if len(os.Args) != 2 {
        fmt.Fprintf(os.Stderr, "須要提供 url 參數\n")
        os.Exit(1)
    }
    url := os.Args[1]
    if err := WaitForServer(url); err != nil {
        fmt.Fprintf(os.Stderr, "Site is down: %v\n", err)
        os.Exit(1)
    }
}

這裏的指數退避策略,以及嘗試屢次簡單的超時退出的實現也頗有意思。 操作系統

3、輸出日誌並退出

接着看上面的代碼,若是屢次重試後依然不能成功,調用者可以輸出錯誤而後優雅地中止程序,但通常這樣的處理應該留給主程序部分:命令行

if err := WaitForServer(url); err != nil {
    fmt.Fprintf(os.Stderr, "Site is down: %v\n", err)
    os.Exit(1)
}

一般,若是是庫函數,應該將錯誤傳遞給調用者,除非這個錯誤表示一個內部的一致性錯誤,這意味着庫內部存在 bug。
這裏還有一個更加方便的方法是經過調用 log.Fatalf 實現上面相同的效果。和全部的日誌函數同樣,它默認會將時間和日期做爲前綴添加到錯誤消息前:

if err := WaitForServer(url); err != nil {
    log.Fatalf("Site is down: %v\n", err)
}

這種帶日期時間的默認格式有助於長期運行的服務器,而對於交互式的命令行工具則意義不大。
還能夠自定義命令的名稱做爲 log 包的前綴,而且將日期和時間略去:

log.SetPrefix("wait: ")
log.SetFlags(0)

4、記錄log日誌

在一些錯誤狀況下,只記錄下錯誤信息而後程序繼續運行。一樣地,能夠選擇使用 log 包來增長日誌的經常使用前綴:

if err := Ping(): err != nil {
    log.Printf("Ping failed: %v; networking disabled", err)
}

全部 log 函數都會爲缺乏換行符的日誌補充一個換行符。
或者是,直接輸出到標準錯誤流:

if err := Ping(): err != nil {
    fmt.Fprintf(os.Stderr, "Ping failed: %v; networking disabled\n", err)
}

沒有用 log 函數,因此沒有時間日期,固然也不須要。上面說了,對於交互式的命令工具意義不大。

5、忽略錯誤

在某些罕見的狀況下,還能夠直接安全地忽略掉整個日誌:

dir, err := ioutil.TempDir("", "scratch")
if err != nil {
    return fmt.Errorf("failed to create temp dir: %v", err)
}
// 使用臨時的目錄
os.RemoveAll(dir)  // 忽略錯誤,$TMPDIR 會被週期性刪除

調用 os.RemoveAll 可能會失敗,但程序忽略了這個錯誤,緣由是操做系統會週期性地清理臨時目錄。在這個例子中,有意的拋棄了錯誤,但程序的邏輯看上去就和忘記去處理同樣了。要習慣考慮到每個函數調用可能發生的出錯狀況,當有意忽略一個錯誤的時候,要清楚地註釋一下你的意圖。

error 接口

以前已經使用過 error 類型了,實際上它是一個接口類型,包含一個返回錯誤消息的方法:

type error interface {
    Error() string
}

errors 包

構造 error 最簡單的方法是調用 errors.New,它會返回一個包含指定錯誤消息的新 error 實例。
完整的 errors 包其實只有以下的4行代碼:

package errors

func New(text string) error { return &errorString{text} }

type errorString struct { s string }

func (e *errorString) Error() string { return e.s }

底層的 errorString 類型是一個結構體,而不是像其餘包裏那樣定義字符串的別名類型。這主要是爲了保護它所表示的錯誤值無心間的(或者也多是故意的)更新。
定義的 Error 方法是指針方法,而不是值方法。這樣每次 New 分配的 error 實例都互不相等,即便是一樣的錯誤值,也是不一樣的地址:

fmt.Println(errors.New("TEST") == errors.New("TEST")) // false

這樣能夠避免好比像 io.EOF 這樣重要的錯誤,與僅僅只是包含一樣錯誤消息的一個錯誤相等。

fmt.Errorf

直接調用 errors.New 的狀況比較少,只在直接能取得錯誤值的字符串信息的時候使用:

func startCPUProfile(w io.Writer) error {
    if w == nil {
        return errors.New("nil File")
    }
    return pprof.StartCPUProfile(w)
}

更多的狀況是會獲得一個錯誤值 err,而咱們能夠在這個錯誤值之上作一點包裝,還須要作字符串格式化。有一個更易用的封裝函數 fmt.Errorf,它額外還提供了字符串格式化的功能,因此通常都是用這個:

doc, err := html.Parse(resp.Body)
if err != nil {
    return fmt.Errorf("parseing %s as HTML: %v", url, err)
}
相關文章
相關標籤/搜索