Go 中 io 包的使用方法

前言

在 Go 中,輸入和輸出操作是使用原語實現的,這些原語將數據模擬成可讀的或可寫的字節流。
爲此,Go 的 io 包提供了 io.Readerio.Writer 接口,分別用於數據的輸入和輸出,如圖:

Go 官方提供了一些 API,支持對內存結構文件網絡連接等資源進行操作
本文重點介紹如何實現標準庫中 io.Readerio.Writer 兩個接口,來完成流式傳輸數據。

io.Reader

io.Reader 表示一個讀取器,它將數據從某個資源讀取到傳輸緩衝區。在緩衝區中,數據可以被流式傳輸和使用。

對於要用作讀取器的類型,它必須實現 io.Reader 接口的唯一一個方法 Read(p []byte)
換句話說,只要實現了 Read(p []byte) ,那它就是一個讀取器。

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

Read() 方法有兩個返回值,一個是讀取到的字節數,一個是發生錯誤時的錯誤。
同時,如果資源內容已全部讀取完畢,應該返回 io.EOF 錯誤。

使用 Reader

利用 Reader 可以很容易地進行流式數據傳輸。Reader 方法內部是被循環調用的,每次迭代,它會從數據源讀取一塊數據放入緩衝區 p (即 Read 的參數 p)中,直到返回 io.EOF 錯誤時停止。

下面是一個簡單的例子,通過 string.NewReader(string) 創建一個字符串讀取器,然後流式地按字節讀取:

func main() {
    reader := strings.NewReader("Clear is better than clever")
    p := make([]byte, 4)

    for {
        n, err := reader.Read(p)
        if err != nil{
            if err == io.EOF {
                fmt.Println("EOF:", n)
                break
            }
            fmt.Println(err)
            os.Exit(1)
        }
        fmt.Println(n, string(p[:n]))
    }
}
輸出打印的內容:
4 Clea
4 r is
4  bet
4 ter 
4 than
4  cle
3 ver
EOF: 0

可以看到,最後一次返回的 n 值有可能小於緩衝區大小。

自己實現一個 Reader

上一節是使用標準庫中的 io.Reader 讀取器實現的。
現在,讓我們看看如何自己實現一個。它的功能是從流中過濾掉非字母字符。

type alphaReader struct {
    // 資源
    src string
    // 當前讀取到的位置 
    cur int
}

// 創建一個實例
func newAlphaReader(src string) *alphaReader {
    return &alphaReader{src: src}
}

// 過濾函數
func alpha(r byte) byte {
    if (r >= 'A' && r <= 'Z') || (r >= 'a' && r <= 'z') {
        return r
    }
    return 0
}

// Read 方法
func (a *alphaReader) Read([]byte) (int, error) {
    // 當前位置 >= 字符串長度 說明已經讀取到結尾 返回 EOF
    if a.cur >= len(a.src) {
        return 0, io.EOF
    }

    // x 是剩餘未讀取的長度
    x := len(a.src) - a.cur
    n, bound := 0, 0
    if x >= len(p) {
        // 剩餘長度超過緩衝區大小,說明本次可完全填滿緩衝區
        bound = len(p)
    } else if x < len(p) {
        // 剩餘長度小於緩衝區大小,使用剩餘長度輸出,緩衝區不補滿
        bound = x
    }

    buf := make([]byte, bound)
    for n < bound {
        // 每次讀取一個字節,執行過濾函數
        if char := alpha(a.src[a.cur]); char != 0 {
            buf[n] = char
        }
        n++
        a.cur++
    }
    // 將處理後得到的 buf 內容複製到 p 中
    copy(p, buf)
    return n, nil
}

func main() {
    reader := newAlphaReader("Hello! It's 9am, where is the sun?")
    p := make([]byte, 4)
    for {
        n, err := reader.Read(p)
        if err == io.EOF {
            break
        }
        fmt.Print(string(p[:n]))
    }
    fmt.Println()
}
輸出打印的內容:
HelloItsamwhereisthesun

組合多個 Reader,目的是重用和屏蔽下層實現的複雜度

標準庫已經實現了許多 Reader
使用一個 Reader 作爲另一個 Reader 的實現是一種常見的用法。
這樣做可以讓一個 Reader 重用另一個 Reader 的邏輯,下面展示通過更新 alphaReader 以接受 io.Reader 作爲其來源。

type alphaReader struct {
    // alphaReader 裏組合了標準庫的 io.Reader
    reader io.Reader
}

func newAlphaReader(reader io.Reader) *alphaReader {
    return &alphaReader{reader: reader}
}

func alpha(r byte) byte {
    if (r >= 'A' && r <= 'Z') || (r >= 'a' && r <= 'z') {
        return r
    }
    return 0
}

func (a *alphaReader) Read([]byte) (int, error) {
    // 這行代碼調用的就是 io.Reader
    n, err := a.reader.Read(p)
    if err != nil {
        return n, err
    }
    buf := make([]byte, n)
    for i := 0; i < n; i++ {
        if char := alpha(p[i]); char != 0 {
            buf[i] = char
        }
    }

    copy(p, buf)
    return n, nil
}

func main() {
    //  使用實現了標準庫 io.Reader 接口的 strings.Reader 作爲實現
    reader := newAlphaReader(strings.NewReader("Hello! It's 9am, where is the sun?"))
    p := make([]byte, 4)
    for {
        n, err := reader.Read(p)
        if err == io.EOF {
            break
        }
        fmt.Print(string(p[:n]))
    }
    fmt.Println()
}

這樣做的另一個優點是 alphaReader 能夠從任何 Reader 實現中讀取。
例如,以下代碼展示了 alphaReader 如何與 os.File 結合以過濾掉文件中的非字母字符:

func main() {
    // file 也實現了 io.Reader
    file, err := os.Open("./alpha_reader3.go")
    if err != nil {
        fmt.Println(err)
        os.Exit(1)
    }
    defer file.Close()
    
    // 任何實現了 io.Reader 的類型都可以傳入 newAlphaReader
    // 至於具體如何讀取文件,那是標準庫已經實現了的,我們不用再做一遍,達到了重用的目的
    reader := newAlphaReader(file)
    p := make([]byte, 4)
    for {
        n, err := reader.Read(p)
        if err == io.EOF {
            break
        }
        fmt.Print(string(p[:n]))
    }
    fmt.Println()
}

io.Writer

io.Writer 表示一個編寫器,它從緩衝區讀取數據,並將數據寫入目標資源。

對於要用作編寫器的類型,必須實現 io.Writer 接口的唯一一個方法 Write(p []byte)
同樣,只要實現了 Write(p []byte) ,那它就是一個編寫器。

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

Write() 方法有兩個返回值,一個是寫入到目標資源的字節數,一個是發生錯誤時的錯誤。

使用 Writer

標準庫提供了許多已經實現了 io.Writer 的類型。
下面是一個簡單的例子,它使用 bytes.Buffer 類型作爲 io.Writer 將數據寫入內存緩衝區。

func main() {
    proverbs := []string{
        "Channels orchestrate mutexes serialize",
        "Cgo is not Go",
        "Errors are values",
        "Don't panic",
    }
    var writer bytes.Buffer

    for _, p := range proverbs {
        n, err := writer.Write([]byte(p))
        if err != nil {
            fmt.Println(err)
            os.Exit(1)
        }
        if n != len(p) {
            fmt.Println("failed to write data")
            os.Exit(1)
        }
    }

    fmt.Println(writer.String())
}
輸出打印的內容:
Channels orchestrate mutexes serializeCgo is not GoErrors are valuesDon't panic

自己實現一個 Writer

下面我們來實現一個名爲 chanWriter 的自定義 io.Writer ,它將其內容作爲字節序列寫入 channel

type chanWriter struct {
    // ch 實際上就是目標資源
    ch chan byte
}

func newChanWriter() *chanWriter {
    return &chanWriter{make(chan byte, 1024)}
}

func (w *chanWriter) Chan() <-chan byte {
    return w.ch
}

func (w *chanWriter) Write([]byte) (int, error) {
    n := 0
    // 遍歷輸入數據,按字節寫入目標資源
    for _, b := range p {
        w.ch <- b
        n++
    }
    return n, nil
}

func (w *chanWriter) Close() error {
    close(w.ch)
    return nil
}

func main() {
    writer := newChanWriter()
    go func() {
        defer writer.Close()
        writer.Write([]byte("Stream "))
        writer.Write([]byte("me!"))
    }()
    for c := range writer.Chan() {
        fmt.Printf("%c", c)
    }
    fmt.Println()
}

要使用這個 Writer,只需在函數 main() 中調用 writer.Write()(在單獨的 goroutine 中)。
因爲 chanWriter 還實現了接口 io.Closer ,所以調用方法 writer.Close() 來正確地關閉 channel,以避免發生泄漏和死鎖。

io 包裏其他有用的類型和方法

如前所述,Go 標準庫附帶了許多有用的功能和類型,讓我們可以輕鬆使用流式 io。

os.File

類型 os.File 表示本地系統上的文件。它實現了 io.Readerio.Writer ,因此可以在任何 io 上下文中使用。
例如,下面的例子展示如何將連續的字符串切片直接寫入文件:

func main() {
    proverbs := []string{
        "Channels orchestrate mutexes serialize\n",
        "Cgo is not Go\n",
        "Errors are values\n",
        "Don't panic\n",
    }
    file, err := os.Create("./proverbs.txt")
    if err != nil {
        fmt.Println(err)
        os.Exit(1)
    }
    defer file.Close()

    for _, p := range proverbs {
        // file 類型實現了 io.Writer
        n, err := file.Write([]byte(p))
        if err != nil {
            fmt.Println(err)
            os.Exit(1)
        }
        if n != len(p) {
            fmt.Println("failed to write data")
            os.Exit(1)
        }
    }
    fmt.Println("file write done")
}

同時,io.File 也可以用作讀取器來從本地文件系統讀取文件的內容。
例如,下面的例子展示瞭如何讀取文件並打印其內容:

func main() {
    file, err := os.Open("./proverbs.txt")
    if err != nil {
        fmt.Println(err)
        os.Exit(1)
    }
    defer file.Close()

    p := make([]byte, 4)
    for {
        n, err := file.Read(p)
        if err == io.EOF {
            break
        }
        fmt.Print(string(p[:n]))
    }
}

標準輸入、輸出和錯誤

os 包有三個可用變量 os.Stdoutos.Stdinos.Stderr ,它們的類型爲 *os.File,分別代表 系統標準輸入系統標準輸出系統標準錯誤 的文件句柄。
例如,下面的代碼直接打印到標準輸出:

func main() {
    proverbs := []string{
        "Channels orchestrate mutexes serialize\n",
        "Cgo is not Go\n",
        "Errors are values\n",
        "Don't panic\n",
    }

    for _, p := range proverbs {
        // 因爲 os.Stdout 也實現了 io.Writer
        n, err := os.Stdout.Write([]byte(p))
        if err != nil {
            fmt.Println(err)
            os.Exit(1)
        }
        if n != len(p) {
            fmt.Println("failed to write data")
            os.Exit(1)
        }
    }
}

io.Copy()

io.Copy() 可以輕鬆地將數據從一個 Reader 拷貝到另一個 Writer。
它抽象出 for 循環模式(我們上面已經實現了)並正確處理 io.EOF 和 字節計數。
下面是我們之前實現的簡化版本:

func main() {
    proverbs := new(bytes.Buffer)
    proverbs.WriteString("Channels orchestrate mutexes serialize\n")
    proverbs.WriteString("Cgo is not Go\n")
    proverbs.WriteString("Errors are values\n")
    proverbs.WriteString("Don't panic\n")

    file, err := os.Create("./proverbs.txt")
    if err != nil {
        fmt.Println(err)
        os.Exit(1)
    }
    defer file.Close()

    // io.Copy 完成了從 proverbs 讀取數據並寫入 file 的流程
    if _, err := io.Copy(file, proverbs); err != nil {
        fmt.Println(err)
        os.Exit(1)
    }
    fmt.Println("file created")
}

那麼,我們也可以使用 io.Copy() 函數重寫從文件讀取並打印到標準輸出的先前程序,如下所示:

func main() {
    file, err := os.Open("./proverbs.txt")
    if err != nil {
        fmt.Println(err)
        os.Exit(1)
    }
    defer file.Close()

    if _, err := io.Copy(os.Stdout, file); err != nil {
        fmt.Println(err)
        os.Exit(1)
    }
}

io.WriteString()

此函數讓我們方便地將字符串類型寫入一個 Writer:

func main() {
    file, err := os.Create("./magic_msg.txt")
    if err != nil {
        fmt.Println(err)
        os.Exit(1)
    }
    defer file.Close()
    if _, err := io.WriteString(file, "Go is fun!"); err != nil {
        fmt.Println(err)
        os.Exit(1)
    }
}

使用管道的 Writer 和 Reader

類型 io.PipeWriterio.PipeReader 在內存管道中模擬 io 操作。
數據被寫入管道的一端,並使用單獨的 goroutine 在管道的另一端讀取。
下面使用 io.Pipe() 創建管道的 reader 和 writer,然後將數據從 proverbs 緩衝區複製到io.Stdout

func main() {
    proverbs := new(bytes.Buffer)
    proverbs.WriteString("Channels orchestrate mutexes serialize\n")
    proverbs.WriteString("Cgo is not Go\n")
    proverbs.WriteString("Errors are values\n")
    proverbs.WriteString("Don't panic\n")

    piper, pipew := io.Pipe()

    // 將 proverbs 寫入 pipew 這一端
    go func() {
        defer pipew.Close()
        io.Copy(pipew, proverbs)
    }()

    // 從另一端 piper 中讀取數據並拷貝到標準輸出
    io.Copy(os.Stdout, piper)
    piper.Close()
}

緩衝區 io

標準庫中 bufio 包支持 緩衝區 io 操作,可以輕鬆處理文本內容。
例如,以下程序逐行讀取文件的內容,並以值 '\n' 分隔:

func main() {
    file, err := os.Open("./planets.txt")
    if err != nil {
        fmt.Println(err)
        os.Exit(1)
    }
    defer file.Close()
    reader := bufio.NewReader(file)

    for {
        line, err := reader.ReadString('\n')
        if err != nil {
            if err == io.EOF {
                break
            } else {
                fmt.Println(err)
                os.Exit(1)
            }
        }
        fmt.Print(line)
    }

}

ioutil

io 包下面的一個子包 utilio 封裝了一些非常方便的功能
例如,下面使用函數 ReadFile 將文件內容加載到 []byte 中。

package main

import (
  "io/ioutil"
   ...
)

func main() {
    bytes, err := ioutil.ReadFile("./planets.txt")
    if err != nil {
        fmt.Println(err)
        os.Exit(1)
    }
    fmt.Printf("%s", bytes)
}

總結

本文介紹瞭如何使用 io.Readerio.Writer 接口在程序中實現流式 IO。
閱讀本文後,您應該能夠了解如何使用 io 包來實現 流式傳輸 IO 數據的程序。
其中有一些例子,展示瞭如何創建自己的類型,並實現io.Readerio.Writer

這是一個簡單介紹性質的文章,沒有擴展開來講。
例如,我們沒有深入文件 IO,緩衝 IO,網絡 IO 或格式化 IO(保存用於將來的寫入)。
我希望這篇文章可以讓你瞭解 Go 語言中 流式 IO 的常見用法是什麼。

謝謝!

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/k5KrkG9Zrxroq4ioGxfJxA