[轉]Go 中 io 包的使用方法

 

原文:https://segmentfault.com/a/1190000015591319

 

---------------------------

前言

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

圖片描述

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

io.Reader

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

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

type Reader interface {
    Read(p []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(p []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(p []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(p []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(p []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.Reader 和 io.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.Stdout ,os.Stdin 和 os.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.PipeWriter 和 io.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.Reader 和 io.Writer 接口在程序中實現流式IO。
閱讀本文後,您應該能夠了解如何使用 io 包來實現 流式傳輸IO數據的程序。
其中有一些例子,展示瞭如何創建自己的類型,並實現io.Reader 和 io.Writer 。

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

謝謝!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章