Go十大常見錯誤第9篇:使用檔名稱作為函式輸入

coding進階 發表於 2022-11-24
Go

前言

這是Go十大常見錯誤系列的第9篇:使用檔名稱作為函式輸入。素材來源於Go佈道者,現Docker公司資深工程師Teiva Harsanyi

本文涉及的原始碼全部開源在:Go十大常見錯誤原始碼,歡迎大家關注公眾號,及時獲取本系列最新更新。

問題場景

一個常見錯誤是把檔名作為函式引數,在函式里讀取檔案內容。

比如,我們要實現一個函式,用來統計指定檔案裡有多少空行,很多人最容易聯想到的實現方式如下:

func count(filename string) (int, error) {
    file, err := os.Open(filename)
    if err != nil {
        return 0, errors.Wrapf(err, "unable to open %s", filename)
    }
    defer file.Close()

    scanner := bufio.NewScanner(file)
    count := 0
    for scanner.Scan() {
        if scanner.Text() == "" {
            count++
        }
    }
    return count, nil
}

這段程式碼邏輯很簡單:

  • 檔名作為函式入參
  • 函式里讀取檔案每一行資料,判斷是否為空行,如果是空行就計數+1

這種方式其實也沒大問題,比較好理解。只是可擴充套件性不強,沒有充分利用到Go語言裡關於資料讀寫的介面(interface)型別的優勢。

試想下,如果你想對一個HTTP body裡的內容實現相同的邏輯,那上面的程式碼無法支援,要另外實現一個新的函式。

解決方案

Go語言裡有2個很好的抽象介面(interface),分別是io.Readerio.Writer

和上面函式傳參使用檔名不一樣,我們可以使用io.Reader作為函式的引數型別。

因為檔案、HTTP body、bytes.Buffer都實現了io.Reader,所以

  • 使用io.Reader作為函式引數可以相容不同型別的資料來源。
  • 不同的資料來源,可以統一使用io.Reader型別裡的Read方法來讀取資料。

具體到這個例子裡,我們可以使用bufio.Reader和其ReadLine方法,程式碼如下所示:

func count(reader *bufio.Reader) (int, error) {
    count := 0
    for {
        line, _, err := reader.ReadLine()
        if err != nil {
            switch err {
            default:
                return 0, errors.Wrapf(err, "unable to read")
            case io.EOF:
                return count, nil
            }
        }
        if len(line) == 0 {
            count++
        }
    }
}

err為io.EOF,就表示讀到了空行。

EOF is the error returned by Read when no more input is available. (Read must return EOF itself, not an error wrapping EOF, because callers will test for EOF using ==.) Functions should return EOF only to signal a graceful end of input. If the EOF occurs unexpectedly in a structured data stream, the appropriate error is either ErrUnexpectedEOF or some other error giving more detail.

有了上面的count函式,我們就可以使用如下的方式開啟檔案,計算檔案裡空行的數量。

file, err := os.Open(filename)
if err != nil {
  return errors.Wrapf(err, "unable to open %s", filename)
}
defer file.Close()
count, err := count(bufio.NewReader(file))

這種實現方式可以讓我們在計算邏輯裡不需要關心真正的資料來源。同時,也可以方便我們做單元測試。

比如下面的例子,我們直接把字串作為輸入,來測試上面實現的count函式。

count, err := count(bufio.NewReader(strings.NewReader("input")))

推薦閱讀

開源地址

文章和示例程式碼開源在GitHub: Go語言初級、中級和高階教程

公眾號:coding進階。關注公眾號可以獲取最新Go面試題和技術棧。

個人網站:Jincheng's Blog

知乎:無忌

福利

我為大家整理了一份後端開發學習資料禮包,包含程式語言入門到進階知識(Go、C++、Python)、後端開發技術棧、面試題等。

關注公眾號「coding進階」,傳送訊息 backend 領取資料禮包,這份資料會不定期更新,加入我覺得有價值的資料。還可以傳送訊息「進群」,和同行一起交流學習,答疑解惑。

References