如何優化Golang中重複的錯誤處理

Golang 錯誤處理最讓人頭疼的問題就是代碼裏充斥着「if err != nil」,它們破壞了代碼的可讀性,本文收集了幾個例子,讓你們明白如何優化此類問題。bash

讓咱們看看 Errors are values 中提到的一個 io.Writer 例子:ide

_, err = fd.Write(p0[a:b])
if err != nil {
    return err
}
_, err = fd.Write(p1[c:d])
if err != nil {
    return err
}
_, err = fd.Write(p2[e:f])
if err != nil {
    return err
}
複製代碼

如上代碼乍一看沒法直觀的看出其原本的意圖是什麼,改進版:優化

type errWriter struct {
	w   io.Writer
	err error
}
func (ew *errWriter) write(buf []byte) {
	if ew.err != nil {
		return
	}
	_, ew.err = ew.w.Write(buf)
}
ew := &errWriter{w: fd}
ew.write(p0[a:b])
ew.write(p1[c:d])
ew.write(p2[e:f])
if ew.err != nil {
    return ew.err
}
複製代碼

經過自定義類型 errWriter 來封裝 io.Writer,而且封裝了 error,新類型有一個 write 方法,不過其方法簽名並無返回 error,而是在方法內部判斷一旦有問題就馬上返回,有了這些準備工做,咱們就能夠把本來穿插在業務邏輯中間的錯誤判斷提出來放到最後來統一調用,從而在視覺上保證讓人能夠直觀的看出代碼原本的意圖是什麼。ui

讓咱們再看看 Eliminate error handling by eliminating errors 中提到的另外一個 io.Writer 例子:spa

type Header struct {
	Key, Value string
}
type Status struct {
	Code   int
	Reason string
}
func WriteResponse(w io.Writer, st Status, headers []Header, body io.Reader) error {
	_, err := fmt.Fprintf(w, "HTTP/1.1 %d %s\r\n", st.Code, st.Reason)
	if err != nil {
		return err
	}
	for _, h := range headers {
		_, err := fmt.Fprintf(w, "%s: %s\r\n", h.Key, h.Value)
		if err != nil {
			return err
		}
	}
	if _, err := fmt.Fprint(w, "\r\n"); err != nil {
		return err
	}
	_, err = io.Copy(w, body)
	return err
}
複製代碼

第一感受既然錯誤是 fmt.Fprint 和 io.Copy 返回的,是否是咱們要從新封裝一下它們?實際上真正的源頭是它們的參數 io.Writer,由於直接調用 io.Writer 的 Writer 方法的話,方法簽名中有返回值 error,因此每一步 fmt.Fprint 和 io.Copy 操做都不得不進行重複的錯誤處理,看上去是壞味道,改進版:code

type errWriter struct {
	io.Writer
	err error
}
func (e *errWriter) Write(buf []byte) (int, error) {
	if e.err != nil {
		return 0, e.err
	}
	var n int
	n, e.err = e.Writer.Write(buf)
	return n, nil
}
func WriteResponse(w io.Writer, st Status, headers []Header, body io.Reader) error {
	ew := &errWriter{Writer: w}
	fmt.Fprintf(ew, "HTTP/1.1 %d %s\r\n", st.Code, st.Reason)
	for _, h := range headers {
		fmt.Fprintf(ew, "%s: %s\r\n", h.Key, h.Value)
	}
	fmt.Fprint(ew, "\r\n")
	io.Copy(ew, body)
	return ew.err
}
複製代碼

經過自定義類型 errWriter 來封裝 io.Writer,而且封裝了 error,同時重寫了 Writer 方法,雖然方法簽名中仍然有返回值 error,可是咱們單獨保存了一份 error,而且在方法內部判斷一旦有問題就馬上返回,有了這些準備工做,新版的 WriteResponse 再也不有重複的錯誤判斷,只須要在最後檢查一下 error 便可。token

相似的作法在 Golang 標準庫中家常便飯,讓咱們繼續看看 Eliminate error handling by eliminating errors 中提到的一個關於 bufio.Reader 和 bufio.Scanner 的例子:string

func CountLines(r io.Reader) (int, error) {
	var (
		br    = bufio.NewReader(r)
		lines int
		err   error
	)
	for {
		_, err = br.ReadString('\n')
		lines++
		if err != nil {
			break
		}
	}
	if err != io.EOF {
		return 0, err
	}
	return lines, nil
}
複製代碼

咱們構造一個 bufio.Reader,而後在一個循環中調用 ReadString 方法,若是讀到文件結尾,那麼 ReadString 會返回一個錯誤(io.EOF),爲了判斷此類狀況,咱們不得不在每次循環時判斷「if err != nil」,看上去這是壞味道,改進版:it

func CountLines(r io.Reader) (int, error) {
	sc := bufio.NewScanner(r)
	lines := 0
	for sc.Scan() {
		lines++
	}
	return lines, sc.Err()
}
複製代碼

實際上,和 bufio.Reader 相比,bufio.Scanner 是一個更高階的類型,換句話簡單點來講的話,至關因而 bufio.Scanner 抽象了 bufio.Reader,經過把低階的 bufio.Reader 換成高階的 bufio.Scanner,循環中再也不須要判斷「if err != nil」,由於 Scan 方法簽名再也不返回 error,而是返回 bool,當在循環裏讀到了文件結尾的時候,循環直接結束,如此一來,咱們就能夠統一在最後調用 Err 方法來判斷成功仍是失敗,看看 Scanner 的定義:io

type Scanner struct {
	r            io.Reader // The reader provided by the client.
	split        SplitFunc // The function to split the tokens.
	maxTokenSize int       // Maximum size of a token; modified by tests.
	token        []byte    // Last token returned by split.
	buf          []byte    // Buffer used as argument to split.
	start        int       // First non-processed byte in buf.
	end          int       // End of data in buf.
	err          error     // Sticky error.
	empties      int       // Count of successive empty tokens.
	scanCalled   bool      // Scan has been called; buffer is in use.
	done         bool      // Scan has finished.
}
複製代碼

可見 Scanner 封裝了 io.Reader,而且封裝了 error,和咱們以前討論的作法一致。有一點說明一下,實際上查看 Scan 源代碼的話,你會發現它不是經過 err 來判斷是否結束的,而是經過 done 來判斷是否結束,這是由於 Scan 只有遇到文件結束的錯誤才退出,其它錯誤會繼續執行,固然,這只是具體的細節問題,不影響咱們的結論。

經過對以上幾個例子的分析,咱們能夠得出優化重複錯誤處理的大概套路:經過建立新的類型來封裝本來幹髒活累活的舊類型,同時在新類型中封裝 error,新舊類型的方法簽名能夠保持兼容,也能夠不兼容,這個不是關鍵的,視客觀狀況而定,至於具體的邏輯實現,先判斷有沒有 error,若是有就直接退出,若是沒有就繼續執行,而且在執行過程當中保存可能出現的 error 以便後面操做使用,最後經過統一調用新類型的 error 來完成錯誤處理。提醒一下,此方案的缺點是要到最後才能知道有沒有錯誤,好在如此的控制粒度在多數時候並沒有大礙。


原文連接:https://huoding.com/2019/04/11/728

相關文章
相關標籤/搜索