簡(jiǎn)單的錯(cuò)誤處理是使用 Fprintf 和 %v 在標(biāo)準(zhǔn)錯(cuò)誤流上輸出一條消息,%v 可以使用默認(rèn)格式顯示任意類(lèi)型的值。
為了保持示例代碼簡(jiǎn)短,有時(shí)會(huì)對(duì)錯(cuò)誤處理有意進(jìn)行一定程度的忽略。明顯的錯(cuò)誤還是要處理的。但是有些出現(xiàn)概率很小的錯(cuò)誤,就忽略了,不過(guò)要標(biāo)記所跳過(guò)的錯(cuò)誤檢查,就是加上注釋。
創(chuàng)新互聯(lián)公司主營(yíng)孟連網(wǎng)站建設(shè)的網(wǎng)絡(luò)公司,主營(yíng)網(wǎng)站建設(shè)方案,成都app軟件開(kāi)發(fā)公司,孟連h5成都微信小程序搭建,孟連網(wǎng)站營(yíng)銷(xiāo)推廣歡迎孟連等地區(qū)企業(yè)咨詢(xún)
根據(jù)情形,將有許多可能的處理場(chǎng)景,接下來(lái)是5個(gè)例子。
最常見(jiàn)的情形是將錯(cuò)誤傳遞下去,使得在子例程中發(fā)生的錯(cuò)誤變?yōu)橹髡{(diào)例程的錯(cuò)誤。
一種是不做任何操作立即向調(diào)用者返回錯(cuò)誤:
resp, err := http.Get(url)
if err != nil {
return nil, err
}
還有一種,不會(huì)直接返回,因?yàn)殄e(cuò)誤信息中缺失一些關(guān)鍵信息:
doc, err := html.Parse(resp.Body)
resp.Body.Close()
if err != nil {
return nil, fmt.Errorf("parsing %s as HTML: %v\n", url, err)
}
這里格式化了一條錯(cuò)誤消息并且返回一個(gè)新的錯(cuò)誤值??梢詾樵嫉腻e(cuò)誤消息不斷地添加上下文信息來(lái)建立一個(gè)可讀的錯(cuò)誤描述。當(dāng)錯(cuò)誤最終被程序的 main 函數(shù)處理時(shí),它應(yīng)該能夠提供一個(gè)從最根本問(wèn)題到總體故障的清晰因果鏈、這里有一個(gè) NASA 的事故調(diào)查的例子:
genesis: crashed: no parachute: G-switch failed: bad relay orientation
因?yàn)殄e(cuò)誤頻繁地串聯(lián)起來(lái),所以消息字符串首字母不應(yīng)該大寫(xiě)而且應(yīng)該避免換行。錯(cuò)誤結(jié)果可能會(huì)很長(zhǎng),但能能夠使用 grep 這樣的工具找到需要的信息。
需要添加的關(guān)鍵信息
有時(shí)候可以不用添加信息直接返回,有時(shí)候需要添加一些關(guān)鍵信息,因?yàn)殄e(cuò)誤信息里沒(méi)有。比如 os.Open 打開(kāi)文件時(shí),返回的錯(cuò)誤不僅僅包括錯(cuò)誤的信息,還包含文件的名字,因此調(diào)用者構(gòu)造錯(cuò)誤消息的時(shí)候不需要包含文件的名字這類(lèi)信息。具體哪些信息是缺少的關(guān)鍵信息需要在原始的錯(cuò)誤消息的基礎(chǔ)上添加?
一般地,f(x) 調(diào)用只負(fù)責(zé)報(bào)告函數(shù)的行為 f 和參數(shù)值 x,因?yàn)樗鼈兒湾e(cuò)誤的上下文相關(guān)。調(diào)用者則負(fù)責(zé)添加進(jìn)一步的信息,但是 f(x) 本身并不會(huì),并且在函數(shù)內(nèi)部也沒(méi)有這些信息。
比如上面的 html.Parse 返回的錯(cuò)誤信息里不可能有 url 的信息,但是,是關(guān)鍵信息需要添加。而 os.Open 中,文件名字也是關(guān)鍵信息,但是這個(gè)正是函數(shù)的參數(shù)值,所以函數(shù)本身會(huì)返回這個(gè)信息,不需要另外添加。
對(duì)于不固定或者不可預(yù)測(cè)的錯(cuò)誤,在短暫的間隔后對(duì)操作進(jìn)行重試是合乎情理的。超出一定的重試次數(shù)和限定的時(shí)間后再報(bào)錯(cuò)退出。
下面給出了完整的代碼,暫時(shí)只看 WaitForServer 函數(shù):
package main
import (
"fmt"
"log"
"net/http"
"os"
"time"
)
// 嘗試連接 url 對(duì)應(yīng)的服務(wù)器
// 在一分鐘內(nèi)使用指數(shù)退避策略進(jìn)行重試
// 所有的嘗試失敗后返回錯(cuò)誤
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)) // 指數(shù)退避策略
}
return fmt.Errorf("server %s failed to respond after %s", url, timeout)
}
func main() {
if len(os.Args) != 2 {
fmt.Fprintf(os.Stderr, "需要提供 url 參數(shù)\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)
}
}
這里的指數(shù)退避策略,以及嘗試多次簡(jiǎn)單的超時(shí)退出的實(shí)現(xiàn)也很有意思。
接著看上面的代碼,如果多次重試后依然不能成功,調(diào)用者能夠輸出錯(cuò)誤然后優(yōu)雅地停止程序,但一般這樣的處理應(yīng)該留給主程序部分:
if err := WaitForServer(url); err != nil {
fmt.Fprintf(os.Stderr, "Site is down: %v\n", err)
os.Exit(1)
}
通常,如果是庫(kù)函數(shù),應(yīng)該將錯(cuò)誤傳遞給調(diào)用者,除非這個(gè)錯(cuò)誤表示一個(gè)內(nèi)部的一致性錯(cuò)誤,這意味著庫(kù)內(nèi)部存在 bug。
這里還有一個(gè)更加方便的方法是通過(guò)調(diào)用 log.Fatalf 實(shí)現(xiàn)上面相同的效果。和所有的日志函數(shù)一樣,它默認(rèn)會(huì)將時(shí)間和日期作為前綴添加到錯(cuò)誤消息前:
if err := WaitForServer(url); err != nil {
log.Fatalf("Site is down: %v\n", err)
}
這種帶日期時(shí)間的默認(rèn)格式有助于長(zhǎng)期運(yùn)行的服務(wù)器,而對(duì)于交互式的命令行工具則意義不大。
還可以自定義命令的名稱(chēng)作為 log 包的前綴,并且將日期和時(shí)間略去:
log.SetPrefix("wait: ")
log.SetFlags(0)
在一些錯(cuò)誤情況下,只記錄下錯(cuò)誤信息然后程序繼續(xù)運(yùn)行。同樣地,可以選擇使用 log 包來(lái)增加日志的常用前綴:
if err := Ping(): err != nil {
log.Printf("Ping failed: %v; networking disabled", err)
}
所有 log 函數(shù)都會(huì)為缺少換行符的日志補(bǔ)充一個(gè)換行符。
或者是,直接輸出到標(biāo)準(zhǔn)錯(cuò)誤流:
if err := Ping(): err != nil {
fmt.Fprintf(os.Stderr, "Ping failed: %v; networking disabled\n", err)
}
沒(méi)有用 log 函數(shù),所以沒(méi)有時(shí)間日期,當(dāng)然也不需要。上面說(shuō)了,對(duì)于交互式的命令工具意義不大。
在某些罕見(jiàn)的情況下,還可以直接安全地忽略掉整個(gè)日志:
dir, err := ioutil.TempDir("", "scratch")
if err != nil {
return fmt.Errorf("failed to create temp dir: %v", err)
}
// 使用臨時(shí)的目錄
os.RemoveAll(dir) // 忽略錯(cuò)誤,$TMPDIR 會(huì)被周期性刪除
調(diào)用 os.RemoveAll 可能會(huì)失敗,但程序忽略了這個(gè)錯(cuò)誤,原因是操作系統(tǒng)會(huì)周期性地清理臨時(shí)目錄。在這個(gè)例子中,有意的拋棄了錯(cuò)誤,但程序的邏輯看上去就和忘記去處理一樣了。要習(xí)慣考慮到每一個(gè)函數(shù)調(diào)用可能發(fā)生的出錯(cuò)情況,當(dāng)有意忽略一個(gè)錯(cuò)誤的時(shí)候,要清楚地注釋一下你的意圖。
之前已經(jīng)使用過(guò) error 類(lèi)型了,實(shí)際上它是一個(gè)接口類(lèi)型,包含一個(gè)返回錯(cuò)誤消息的方法:
type error interface {
Error() string
}
構(gòu)造 error 最簡(jiǎn)單的方法是調(diào)用 errors.New,它會(huì)返回一個(gè)包含指定錯(cuò)誤消息的新 error 實(shí)例。
完整的 errors 包其實(shí)只有如下的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 類(lèi)型是一個(gè)結(jié)構(gòu)體,而不是像其他包里那樣定義字符串的別名類(lèi)型。這主要是為了保護(hù)它所表示的錯(cuò)誤值無(wú)意間的(或者也可能是故意的)更新。
定義的 Error 方法是指針?lè)椒ǎ皇侵捣椒?。這樣每次 New 分配的 error 實(shí)例都互不相等,即使是同樣的錯(cuò)誤值,也是不同的地址:
fmt.Println(errors.New("TEST") == errors.New("TEST")) // false
這樣可以避免比如像 io.EOF 這樣重要的錯(cuò)誤,與僅僅只是包含同樣錯(cuò)誤消息的一個(gè)錯(cuò)誤相等。
直接調(diào)用 errors.New 的情況比較少,只在直接能取得錯(cuò)誤值的字符串信息的時(shí)候使用:
func startCPUProfile(w io.Writer) error {
if w == nil {
return errors.New("nil File")
}
return pprof.StartCPUProfile(w)
}
更多的情況是會(huì)得到一個(gè)錯(cuò)誤值 err,而我們可以在這個(gè)錯(cuò)誤值之上做一點(diǎn)包裝,還需要做字符串格式化。有一個(gè)更易用的封裝函數(shù) fmt.Errorf,它額外還提供了字符串格式化的功能,所以一般都是用這個(gè):
doc, err := html.Parse(resp.Body)
if err != nil {
return fmt.Errorf("parseing %s as HTML: %v", url, err)
}