go io.reader 讀取string_[譯]Go 語言中的流式 IO
原文連結
- Streaming IO in Go – Learning the Go Programming Language – Medium
以下為譯文
在 Go 中,輸入輸出操作是通過能讀能寫的位元組流資料模型來實現的。為此,io 包提供了 io.Reader 和 io.Writer 介面來進行輸入輸出操作,如下所示:
Go 附帶了許多 API,這些 API 支援來自記憶體結構,檔案,網路連線等資源的流式 IO。本文重點介紹如何自定義實現以及使用標準庫中的 io.Reader 和 io.Writer介面建立能夠傳輸流式資料的 Go 程式
io.Reader
由 io.Reader 介面表示的讀取器將資料從某些源讀取到緩衝區,可以像用水管輸送水流一樣來傳送它,如下所示
對於要用作讀取器的型別,它必須從介面 io.Reader 實現 Read(p [] byte)方法,如下所示:
type Reader interface {
Read(p []byte) (n int, err error)
}
Read() 方法的實現應返回讀取的位元組數或發生的錯誤。如果資料來源已輸出全部內容,則 Read 應返回 io.EOF
讀取規則(補充)
在 Reddit 反饋之後,我決定新增有關讀取規則的這一部分。讀取器的行為取決於它的實現,但是你應該知道從讀取器讀取資料時, io.Reader 中的一些規則:
譯者注:p 為緩衝區,n 為位元組數
- 如果可能,Read() 將讀取 len(p) 到 p
- 呼叫 Read() 後,返回的位元組數 n 可能小於 len(p)
- 出錯時,Read() 仍可在緩衝區 p 中返回 n 個位元組。例如,從突然關閉的 TCP 套接字讀取。取決於您的程式設計,您可以選擇將位元組儲存在 p 中或重新嘗試從 TCP 套接字中讀取
- 當 Read() 讀完所有可用資料時,讀取器可能返回非零 n 和 err = io.EOF。儘管如此,您可以自己實現返回規則,如可以選擇在流的末尾返回非零 n 和 err = nil。在這種情況下,任何後續讀取必須返回 n = 0,err = io.EOF
- 最後,呼叫 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 表示的寫入器從緩衝區流式傳輸資料並將其寫入目標資源,如下所示
所有流寫入器必須從介面 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 serializen",
"Cgo is not Gon",
"Errors are valuesn",
"Don't panicn",
}
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 serializen",
"Cgo is not Gon",
"Errors are valuesn",
"Don't panicn",
}
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 serializen")
proverbs.WriteString("Cgo is not Gon")
proverbs.WriteString("Errors are valuesn")
proverbs.WriteString("Don't panicn")
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 serializen")
proverbs.WriteString("Cgo is not Gon")
proverbs.WriteString("Errors are valuesn")
proverbs.WriteString("Don't panicn")
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 慣用語是什麼