Go - io 標準函式庫學習筆記

October 17, 2017

關於 Go 語言 io 標準函式庫的一些閱讀後的筆記

Reader 的定義

  • Reader 是一個包裝了基本 Read 方法的 interface。

  • Read 讀取 len(p) 位元組到 p,它會回傳位元組數 n0 <= n <= len(p))和任何遇到的錯誤。即使 Read 回傳的 n 小於 len(p),它也會在調用過程中,使用 p 作為暫存空間。如果一些資料可以使用,但是不滿足 len(p) 位元組長度,一般 Read 會回傳可使用的資料,而不是繼續等待更多的資料。

  • 當 Read 在成功讀取 n > 0 位元組遇到錯誤,或者是 EOF(end-of-file) 狀態時,它會回傳讀取到的位元組數。它會從相同的調用回傳非 nil 的錯誤或者是從一個隨後調用中回傳錯誤(n 同時為 0)。一般常見的情況是,一個 Reader 在 input stream 結束時,回傳一個非 0 的位元組長度,可能是 err == EOF 或是 err == nil。下一個 Read 應該回傳 0, EOF

  • Callers 總是要考慮到在錯誤 err 之前,應該處理回傳的 n > 0 位元組長度。這樣做可以正確處理在讀取一些位元組後所發生的 I/O 錯誤,同時也允許 EOF 行為。

  • Read 的實作不希望回傳一個 0 位元組的長度和一個 nil 的 error,除非當 len(p) == 0。Callers 應該對於一個回傳 0nil 作為什麼事都沒發生,特別是它並不表示是 EOF。

Reader 的 interface 定義:

type Reader interface {
    Read(p []byte) (n int, err error)
}

範例:

// Source: https://github.com/polaris1119/The-Golang-Standard-Library-by-Example/blob/master/chapter01/01.1.md

func ReadFrom(reader io.Reader, num int) ([]byte, error) {
    p := make([]byte, num)
    n, err := reader.Reader
    if n > 0 {
        retrun p[:n], nil
    }

    return p, err
}

func main() {
    data, err := ReadFrom(strings.NewReader("Write Something...", 20))
    if err != nil {
      log.Fatal(err)
    }

    fmt.Println(string(data))
}

ReadFrom 函式接受 readernum 參數,其中 reader 的型別是 io.Reader,所以只要有實作 io.Reader 這個型別都可以作為參數傳入,函式最後回傳了 ([]byte, error)

ReadFrom 函式的第五行,我們可以取得實作 io.Readerreader 所回傳的位元組長度以及錯誤;在第十四行,我們使用了 strings 標準函式庫的 NewReader 方法,根據文件說明 strings.NewReader 回傳一個新的 Reader,詳細請參考原始碼,所以這符合傳入 ReadFrom 的第一個參數所要求的型別。

Writer 的定義

  • Writer 是一個包裝基本 Write 方法的 interface。

  • Write 從 p 寫入 len(p) 位元組長度到底層的 data stream。它從 p 回傳 (0 <= n <= len(p)) 的位元組數以及任何遇到造成寫入提早停止的任何錯誤。

  • 如果回傳 n < len(p),Write 必須回傳一個非 nil 的錯誤。

  • 即使暫停,Write 也不能修改 slice 資料。

Writeer 的 interface 定義:

type Writer interface {
    Write(p []byte) (n int, err error)
}

範例:

func main() {
      var b bytes.Buffer
      b.Write([]byte("Hello! "))
      fmt.Fprint(&b, "My name is P.J.")
      b.WriteTo(os.Stdout)
}

這裡我們使用 bytes.Buffer 作為範例,因為 Buffer 實作了 io 的方法,在第三行我們對 Write 方法傳入了一個 slice,fmt.Fprint 第一個參數接收一個 io.Writer 型別的參數,它會將第二個參數的內容寫入到第一個參數的 slice 內,實際上 fmt.Fprint 的第二個參數是一個可變參數,也就是說我也可以傳入像是 fmt.Fprint(&b, 1, "Hi", 2, "Ho") 帶有多個參數,詳細可以參考 fmt.Fprint定義,最後我們使用了 b.WriteTo(os.Stdout) 輸出訊息到終端機。

os.Stdout 是來自 os 標準函式庫內的變數

var (
    Stdin  = NewFile(uintptr(syscall.Stdin), "/dev/stdin")
    Stdout = NewFile(uintptr(syscall.Stdout), "/dev/stdout")
    Stderr = NewFile(uintptr(syscall.Stderr), "/dev/stderr")
)

實際上,StdinStdoutStderr 是對應到 Linux 下的輸入、輸出,和錯誤描述,根據 NewFile 的定義,它們都回傳了 *File 這個 type,而 *File 也都實作了 io 的 Reader 和 Writer

Go 的 io 標準函式庫提供了最基本的 interface,它主要的工作是封裝一些現有原始(primitive)的實作,把抽象的功能轉換爲公開的 interface,所以我們並不需要關心具體的邏輯。

心得

我們在撰寫程式碼時,需要注意方法所接收的參數型別,例如某個函式的定義是 func someFn(r io.Reader),那麼我們傳入的 r 就必須一定就必須一定要實作 io.Reader,我們不需要關心實際的實作細節,例如在 Reader 的範例;如果同時實作了 io.Readerio.Writer,就可以進行資料的讀取和寫入囉。

Reference