[譯]Go 語言中的流式 IO

原文鏈接

以下為譯文

在 Go 中,輸入輸出操作是通過能讀能寫的字節流數據模型來實現的。為此,io 包提供了 io.Reader 和 io.Writer 接口來進行輸入輸出操作,如下所示:

image

Go 附帶了許多 API,這些 API 支持來自內存結構,文件,網絡連接等資源的流式 IO。本文重點介紹如何自定義實現以及使用標準庫中的 io.Reader 和 io.Writer接口創建能夠傳輸流式數據的 Go 程序

io.Reader

由 io.Reader 接口表示的讀取器將數據從某些源讀取到緩沖區,可以像用水管輸送水流一樣來傳送它,如下所示

image

對于要用作讀取器的類型,它必須從接口 io.Reader 實現 Read(p [] byte)方法,如下所示:

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

Read() 方法的實現應返回讀取的字節數或發生的錯誤。如果數據源已輸出全部內容,則 Read 應返回 io.EOF

讀取規則(補充)

在 Reddit 反饋之后,我決定添加有關讀取規則的這一部分。讀取器的行為取決于它的實現,但是你應該知道從讀取器讀取數據時, io.Reader 中的一些規則:

譯者注:p 為緩沖區,n 為字節數

  1. 如果可能,Read() 將讀取 len(p) 到 p
  2. 調用 Read() 后,返回的字節數 n 可能小于 len(p)
  3. 出錯時,Read() 仍可在緩沖區 p 中返回 n 個字節。例如,從突然關閉的 TCP 套接字讀取。取決于您的程序設計,您可以選擇將字節保存在 p 中或重新嘗試從 TCP 套接字中讀取
  4. 當 Read() 讀完所有可用數據時,讀取器可能返回非零 n 和 err = io.EOF。盡管如此,您可以自己實現返回規則,如可以選擇在流的末尾返回非零 n 和 err = nil。在這種情況下,任何后續讀取必須返回 n = 0,err = io.EOF
  5. 最后,調用 Read() 返回 n = 0 和 err = nil 并不意味著 EOF,因為下一次調用 Read() 可能會返回更多數據

如您所見,直接從讀取器讀取流數據可能會非常棘手。幸運的是,標準庫中的讀取器使用的一些方法使其易于流式傳輸。不過,在使用讀取器之前,請查閱其文檔

從讀取器中流式傳輸數據

直接從讀取器流式傳輸數據很容易。Read 方法被設計為在循環內調用,每次迭代時,它從源讀取一大塊數據并將其放入緩沖區 p 中。直到 Read 方法返回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 == io.EOF {
            break
        }
        fmt.Println(string(p[:n]))
    }
}

上面的源代碼用 make([] byte,4) 創建一個 4 字節長的傳輸緩沖區 p。緩沖區故意保持小于字符串源的長度, 這是為了演示如何從大于緩沖區的源正確傳輸數據塊

更新: Reddit 上有人指出上面的代碼中有 bug, 它永遠不會捕獲非零錯誤 err != io.EOF . 以下修復了代碼:

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(string(p[:n])) //should handle any remainding bytes.
            break
            }
            fmt.Println(err)
            os.Exit(1)
        }
        fmt.Println(string(p[:n]))
    }
}

自定義一個 io.Reader

上一節使用標準庫中的現有 IO 讀取器實現。現在,讓我們看看如何編寫自己的讀取器。以下是 io.Reader 的簡單實現,它從流中過濾掉非字母字符。

package main

import (
    "fmt"
    "io"
)

// alphaReader is a simple implementation of an io.Reader
// that streams only alpha chars from its string source.
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
}

func (a *alphaReader) Read(p []byte) (int, error) {
    if a.cur >= len(a.src) {
        return 0, io.EOF
    }

    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++
    }
    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]))
    }
    // or use io.Copy
    // io.Copy(os.Stdout, reader)
    fmt.Println()
}

程序執行時,輸出:

$> go run alpha_reader.go
HelloItsamwhereisthesun

鏈式讀取器

標準庫已經實現了許多讀取器。使用讀取器作為另一個讀取器的源是一種常見的習語。讀取器的這種鏈接允許一個讀取器重用另一個讀取器的邏輯,就像在下面的源代碼片段中所做的那樣,更新 alphaReader 以接受 io.Reader 作為其源。這通過將流管理問題推向根讀取器來降低代碼的復雜性。

package main

import (
    "fmt"
    "io"
    "strings"
)

// alphaReader is a simple implementation of an io.Reader
// that streams only alpha chars from its string source.
// This example uses another reader as data source.
type alphaReader struct {
    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) {
    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() {
    // use an io.Reader as source for alphaReader
    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 現在能夠從任何讀取器實現中讀取。例如,以下代碼段顯示了如何將 alphaReader 與 os.File 源結合以過濾掉文件中的非字母字符:

package main

import (
    "fmt"
    "io"
    "os"
)

// alphaReader is a simple implementation of an io.Reader
// that streams only alpha chars from its string source.
// This example uses another reader as data source.
type alphaReader struct {
    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) {
    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() {
    // use an io.Reader as source for alphaReader
    file, err := os.Open("./alpha_reader2.go")
    if err != nil {
        fmt.Println(err)
        os.Exit(1)
    }
    defer file.Close()

    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 表示的寫入器從緩沖區流式傳輸數據并將其寫入目標資源,如下所示

image

所有流寫入器必須從接口 io.Writer 實現方法 Write(p [] byte)。該方法旨在從緩沖區 p 讀取數據并將其寫入指定的目標資源

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

Write() 方法的實現應返回寫入的字節數或發生的錯誤

使用寫入器

標準庫附帶了許多預先實現的 io.Writer 類型。直接使用寫入器很簡單,如下面的代碼片段所示,它使用 bytes.Buffer 作為 io.Writer 將數據寫入內存緩沖區

package main

import (
    "bytes"
    "fmt"
    "os"
)

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())
}

自定義一個 io.Writer

本節中的代碼顯示了如何實現一個名為 chanWriter 的自定義 io.Writer,它將其內容作為字節序列寫入 Go 通道。

package main

import "fmt"

type chanWriter struct {
    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()
}

要使用寫入器,代碼只需在函數 main() 中調用方法 writer.Write() (在單獨的goroutine 中)。因為 chanWriter 還實現了接口 io.Closer,所以調用方法writer.Close() 來正確關閉通道,以避免在訪問通道時出現死鎖

Useful types and packages for IO

如前所述,Go 標準庫附帶了許多有用的功能和其他類型,可以輕松使用流式IO

os.File

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

package main

import (
    "fmt"
    "os"
)

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 {
        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 類型可以用作讀取器來從本地文件系統流式傳輸文件的內容。例如,以下源代碼段讀取文件并打印其內容:

package main

import (
    "fmt"
    "io"
    "os"
)

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]))
    }
}

Standard output, input, and error

os 包公開三個變量,os.Stdout,os.Stdin 和 os.Stderr,它們的類型為* os.File,分別表示操作系統標準輸出\輸入\錯誤的文件句柄。例如,以下源代碼段直接打印到標準輸出:

package main

import (
    "fmt"
    "os"
)

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 {
        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() 方法可以輕松地將數據從源讀取器傳輸到目標寫入器。它抽象出 for 循環模式(我們到目前為止已經看到)并正確處理 io.EOF 和字節計數。

以下顯示了以前程序的簡化版本,該程序復制內存讀取器 proberbs 的內容并將其復制到 writer 文件:

package main

import (
    "bytes"
    "fmt"
    "io"
    "os"
)

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()

    // copy from reader data into writer file
    if _, err := io.Copy(file, proverbs); err != nil {
        fmt.Println(err)
        os.Exit(1)
    }
    fmt.Println("file created")
}

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

package main

import (
    "fmt"
    "io"
    "os"
)

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.WriterString()

此函數提供了將字符串值寫入指定寫入器的便利,如下所示

package main

import (
    "fmt"
    "io"
    "os"
)

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)
    }
}

Pipe writers and readers

io.PipeWriter 類型和 io.PipeReader 模型 IO 操作在內存管道中。數據被寫入管道的 writer-end,并使用單獨的 go 例程在管道的 reader-end 上讀取。下面使用 io.Pipe() 創建管道讀取器/寫入器對,然后使用 io.Pipe() 將數據從緩沖區 proverbs 復制到 io.Stdout, 如下所示

package main

import (
    "bytes"
    "io"
    "os"
)

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()

    // write in writer end of pipe
    go func() {
        defer pipew.Close()
        io.Copy(pipew, proverbs)
    }()

    // read from reader end of pipe.
    io.Copy(os.Stdout, piper)
    piper.Close()
}

Buffered IO

Go 通過 bufio 包支持緩沖 IO,可以輕松處理文本內容。例如,以下程序逐行讀取文件以值 '\ n' 分隔的內容

package main

import (
    "bufio"
    "fmt"
    "io"
    "os"
)

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)
    }

}

Util package

ioutil 包為 IO 提供了幾個便利功能。例如,以下使用函數 ReadFile 將文件內容加載到[]字節中

package main

import (
    "fmt"
    "io/ioutil"
    "os"
)

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 的 Go 包的范圍。例如,我們沒有進入文件 IO,緩沖 IO,網絡 IO或格式化 IO(為將來的寫作而保留)。我希望這能讓你了解 Go 的流式 IO 慣用語是什么

?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,401評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,011評論 3 413
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,263評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,543評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,323評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,874評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,968評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,095評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,605評論 1 331
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,551評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,720評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,242評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,961評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,358評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,612評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,330評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,690評論 2 370

推薦閱讀更多精彩內容