在 Go 中,輸入和輸出操做是使用原語實現的,這些原語將數據模擬成可讀的或可寫的字節流。
爲此,Go 的 io
包提供了 io.Reader
和 io.Writer
接口,分別用於數據的輸入和輸出,如圖:網絡
Go 官方提供了一些 API,支持對內存結構,文件,網絡鏈接等資源進行操做
本文重點介紹如何實現標準庫中 io.Reader
和 io.Writer
兩個接口,來完成流式傳輸數據。函數
io.Reader
io.Reader
表示一個讀取器,它將數據從某個資源讀取到傳輸緩衝區。在緩衝區中,數據能夠被流式傳輸和使用。
如圖:spa
對於要用做讀取器的類型,它必須實現 io.Reader
接口的惟一一個方法 Read(p []byte)
。
換句話說,只要實現了 Read(p []byte)
,那它就是一個讀取器。3d
type Reader interface { Read(p []byte) (n int, err error) }
Read()
方法有兩個返回值,一個是讀取到的字節數,一個是發生錯誤時的錯誤。
同時,若是資源內容已所有讀取完畢,應該返回 io.EOF
錯誤。code
利用 Reader
能夠很容易地進行流式數據傳輸。Reader
方法內部是被循環調用的,每次迭代,它會從數據源讀取一塊數據放入緩衝區 p
(即 Read 的參數 p)中,直到返回 io.EOF
錯誤時中止。blog
下面是一個簡單的例子,經過 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 值有可能小於緩衝區大小。圖片
上一節是使用標準庫中的 io.Reader
讀取器實現的。
如今,讓咱們看看如何本身實現一個。它的功能是從流中過濾掉非字母字符。ip
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
的邏輯,下面展現經過更新 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()
方法有兩個返回值,一個是寫入到目標資源的字節數,一個是發生錯誤時的錯誤。
標準庫提供了許多已經實現了 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
下面咱們來實現一個名爲 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 的常見用法是什麼。
謝謝!