gopl-zh.github.com/ch5/ch5-04-1.md
2016-01-05 23:46:55 +08:00

5.9 KiB
Raw Blame History

5.4.1. 錯誤處理策略

當一次函數調用返迴錯誤時,調用者有應該選擇何時的方式處理錯誤。根據情況的不同,有很多處理方式,讓我們來看看常用的五種方式。

首先也是最常用的方式是傳播錯誤。這意味着函數中某個子程序的失敗會變成該函數的失敗。下面我們以5.3節的findLinks函數作爲例子。如果findLinks對http.Get的調用失敗findLinks會直接將這個HTTP錯誤返迴給調用者

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

當對html.Parse的調用失敗時findLinks不會直接返迴html.Parse的錯誤因爲缺少兩條重要信息1、錯誤發生在解析器2、url已經被解析。這些信息有助於錯誤的處理findLinks會構造新的錯誤信息返迴給調用者

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

fmt.Errorf函數使用fmt.Sprintf格式化錯誤信息併返迴。我們使用該函數前綴添加額外的上下文信息到原始錯誤信息。當錯誤最終由main函數處理時錯誤信息應提供清晰的從原因到後果的因果鏈就像美国宇航局事故調査時做的那樣

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

由於錯誤信息經常是以鏈式組合在一起的所以錯誤信息中應避免大寫和換行符。最終的錯誤信息可能很長我們可以通過類似grep的工具處理錯誤信息譯者註grep是一種文本蒐索工具

編寫錯誤信息時,我們要確保錯誤信息對問題細節的描述是詳盡的。尤其是要註意錯誤信息表達的一致性,卽相同的函數或同包內的同一組函數返迴的錯誤在構成和處理方式上是相似的。

以OS包爲例OS包確保文件操作如os.Open、Read、Write、Close返迴的每個錯誤的描述不僅僅包含錯誤的原因如無權限文件目録不存在也包含文件名這樣調用者在構造新的錯誤信息時無需再添加這些信息。

一般而言被調函數f(x)會將調用信息和參數信息作爲發生錯誤時的上下文放在錯誤信息中併返迴給調用者調用者需要添加一些錯誤信息中不包含的信息比如添加url到html.Parse返迴的錯誤中。

讓我們來看看處理錯誤的第二種策略。如果錯誤的發生是偶然性的,或由不可預知的問題導致的。一個明智的選擇是重新嚐試失敗的操作。在重試時,我們需要限製重試的時間間隔或重試的次數,防止無限製的重試。

gopl.io/ch5/wait
// WaitForServer attempts to contact the server of a URL.
// It tries for one minute using exponential back-off.
// It reports an error if all attempts fail.
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 // success
		}
		log.Printf("server not responding (%s);retrying…", err)
		time.Sleep(time.Second << uint(tries)) //
		exponential back-off
	}
	return fmt.Errorf("server %s failed to respond after %s", url, timeout)
}

如果錯誤發生後程序無法繼續運行我們就可以采用第三種策略輸出錯誤信息併結束程序。需要註意的是這種策略隻應在main中執行。對庫函數而言應僅向上傳播錯誤除非該錯誤意味着程序內部包含不一致性卽遇到了bug才能在庫函數中結束程序。

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

調用log.Fatalf可以更簡潔的代碼達到與上文相同的效果。log中的所有函數都默認會在錯誤信息之前輸出時間信息。

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

長時間運行的服務器常采用默認的時間格式,而交互式工具很少采用包含如此多信息的格式。

2006/01/02 15:04:05 Site is down: no such domain:
bad.gopl.io

我們可以設置log的前綴信息屏蔽時間信息一般而言前綴信息會被設置成命令名。

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

第四種情況有時我們隻需要輸出錯誤信息就足夠了不需要中斷程序的運行。我們可以通過log包提供函數

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

或者標準錯誤流輸出錯誤信息。

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

log包中的所有函數會爲沒有換行符的字符串增加換行符。

第五種,也是最後一種策略:我們可以直接忽略掉錯誤。

dir, err := ioutil.TempDir("", "scratch")
if err != nil {
	return fmt.Errorf("failed to create temp dir: %v",err)
}
// ...use temp dir…
os.RemoveAll(dir) // ignore errors; $TMPDIR is cleaned periodically

盡管os.RemoveAll會失敗但上面的例子併沒有做錯誤處理。這是因爲操作繫統會定期的清理臨時目録。正因如此雖然程序沒有處理錯誤但程序的邏輯不會因此受到影響。我們應該在每次函數調用後都養成考慮錯誤處理的習慣當你決定忽略某個錯誤時你應該在清晰的記録下你的意圖。

在Go中錯誤處理有一套獨特的編碼風格。檢査某個子函數是否失敗後我們通常將處理失敗的邏輯代碼放在處理成功的代碼之前。如果某個錯誤會導致函數返迴那麽成功時的邏輯代碼不應放在else語句塊中而應直接放在函數體中。Go中大部分函數的代碼結構幾乎相同首先是一繫列的初始檢査防止錯誤發生之後是函數的實際邏輯。