前言
在 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 的常見用法是什麼。
謝謝!