Go 流式 IO


原文鏈接:基本的 IO 接口

原文鏈接:方便的IO操作函數集

圖片來源:圖片顯示來源

1. 1.1 io — 基本的 IO 接口

io 包為 I/O 原語提供了基本的接口。它主要包裝了這些原語的已有實現。

由於這些被接口包裝的I/O原語是由不同的低級操作實現,因此,在另有聲明之前不該假定它們的並行執行是安全的。

在 io 包中最重要的是兩個接口:Reader 和 Writer 接口。本章所提到的各種 IO 包,都跟這兩個接口有關,也就是說,只要滿足這兩個接口,它就可以使用 IO 包的功能。

1.1. 概覽

1.2. 邏輯

1.3. Reader 接口

Reader 接口的定義如下:

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

官方文檔中關於該接口方法的說明:

Read 將 len(p) 個字節讀取到 p 中。它返回讀取的字節數 n(0 <= n <= len(p)) 以及任何遇到的錯誤。即使 Read 返回的 n < len(p),它也會在調用過程中占用 len(p) 個字節作為暫存空間。若可讀取的數據不到 len(p) 個字節,Read 會返回可用數據,而不是等待更多數據。

當 Read 在成功讀取 n > 0 個字節后遇到一個錯誤或 EOF (end-of-file),它會返回讀取的字節數。它可能會同時在本次的調用中返回一個non-nil錯誤,或在下一次的調用中返回這個錯誤(且 n 為 0)。 一般情況下, Reader會返回一個非0字節數n, 若 n = len(p) 個字節從輸入源的結尾處由 Read 返回,Read可能返回 err == EOF 或者 err == nil。並且之后的 Read() 都應該返回 (n:0, err:EOF)。

調用者在考慮錯誤之前應當首先處理返回的數據。這樣做可以正確地處理在讀取一些字節后產生的 I/O 錯誤,同時允許EOF的出現。

根據 Go 語言中關於接口和滿足了接口的類型的定義(Interface_types),我們知道 Reader 接口的方法集(Method_sets)只包含一個 Read 方法,因此,所有實現了 Read 方法的類型都滿足 io.Reader 接口,也就是說,在所有需要 io.Reader 的地方,可以傳遞實現了 Read() 方法的類型的實例。

下面,我們通過具體例子來談談該接口的用法。

func ReadFrom(reader io.Reader, num int) ([]byte, error) {
    p := make([]byte, num)
    n, err := reader.Read(p)
    if n > 0 {
        return p[:n], nil
    }
    return p, err
}

ReadFrom 函數將 io.Reader 作為參數,也就是說,ReadFrom 可以從任意的地方讀取數據,只要來源實現了 io.Reader 接口。比如,我們可以從標准輸入、文件、字符串等讀取數據,示例代碼如下:

// 從標准輸入讀取
data, err = ReadFrom(os.Stdin, 11)

// 從普通文件讀取,其中 file 是 os.File 的實例
data, err = ReadFrom(file, 9)

// 從字符串讀取
data, err = ReadFrom(strings.NewReader("from string"), 12)

完整的演示例子源碼見 code/src/chapter01/io/reader.go

另外的例子:(使用go中已經實現該接口的bytes類型)

reader := bytes.NewReader([]byte("hello world"))
p := make([]byte,11)
reader.Read(p)
fmt.Printf("%x\n",p) //68656c6c6f20776f726c64
fmt.Printf("%s\n",p) //hello world 

小貼士

io.EOF 變量的定義:var EOF = errors.New("EOF"),是 error 類型。根據 reader 接口的說明,在 n > 0 且數據被讀完了的情況下,當次返回的 error 有可能是 EOF 也有可能是 nil。

1.4. Writer 接口

Writer 接口的定義如下:

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

官方文檔中關於該接口方法的說明:

Write 將 len(p) 個字節從 p 中寫入到基本數據流中。它返回從 p 中被寫入的字節數 n(0 <= n <= len(p))以及任何遇到的引起寫入提前停止的錯誤。若 Write 返回的 n < len(p),它就必須返回一個 非nil 的錯誤。

同樣的,所有實現了Write方法的類型都實現了 io.Writer 接口。

在上個例子中,我們是自己實現一個函數接收一個 io.Reader 類型的參數。這里,我們通過標准庫的例子來學習。

在fmt標准庫中,有一組函數:Fprint/Fprintf/Fprintln,它們接收一個 io.Wrtier 類型參數(第一個參數),也就是說它們將數據格式化輸出到 io.Writer 中。那么,調用這組函數時,該如何傳遞這個參數呢?

我們以 fmt.Fprintln 為例,同時看一下 fmt.Println 函數的源碼。

func Println(a ...interface{}) (n int, err error) {
    return Fprintln(os.Stdout, a...)
}

很顯然,fmt.Println會將內容輸出到標准輸出中。下一節我們將詳細介紹fmt包。

另外的例子:

s := []byte("hello world")
buf := new(bytes.Buffer)
buf.Write(s)
fmt.Printf("% x\n", buf.Bytes())  //68 65 6c 6c 6f 20 77 6f 72 6c 64
fmt.Printf("% s\n", buf.Bytes())  //hello world

關於 io.Writer 的更多說明,可以查看筆者之前寫的博文《以io.Writer為例看go中的interface{}》

1.5. 實現了 io.Reader 接口或 io.Writer 接口的類型

初學者看到函數參數是一個接口類型,很多時候有些束手無策,不知道該怎么傳遞參數。還有人問:標准庫中有哪些類型實現了 io.Reader 或 io.Writer 接口?

通過本節上面的例子,我們可以知道,os.File 同時實現了這兩個接口。我們還看到 os.Stdin/Stdout 這樣的代碼,它們似乎分別實現了 io.Reader/io.Writer 接口。沒錯,實際上在 os 包中有這樣的代碼:

var (
    Stdin  = NewFile(uintptr(syscall.Stdin), "/dev/stdin")
    Stdout = NewFile(uintptr(syscall.Stdout), "/dev/stdout")
    Stderr = NewFile(uintptr(syscall.Stderr), "/dev/stderr")
)

也就是說,Stdin/Stdout/Stderr 只是三個特殊的文件類型的標識(即都是 os.File 的實例),自然也實現了 io.Reader 和 io.Writer。

目前,Go 文檔中還沒有直接列出實現了某個接口的所有類型。不過,我們可以通過查看標准庫文檔,列出實現了 io.Reader 或 io.Writer 接口的類型(導出的類型):(注:godoc 命令支持額外參數 -analysis ,能列出都有哪些類型實現了某個接口,相關參考 godoc -h 或 Static analysis features of godoc。另外,我做了一個官網鏡像,能查看接口所有的實現類型,地址:http://docs.studygolang.com。

  • os.File 同時實現了 io.Reader 和 io.Writer
  • strings.Reader 實現了 io.Reader
  • bufio.Reader/Writer 分別實現了 io.Reader 和 io.Writer
  • bytes.Buffer 同時實現了 io.Reader 和 io.Writer
  • bytes.Reader 實現了 io.Reader
  • compress/gzip.Reader/Writer 分別實現了 io.Reader 和 io.Writer
  • crypto/cipher.StreamReader/StreamWriter 分別實現了 io.Reader 和 io.Writer
  • crypto/tls.Conn 同時實現了 io.Reader 和 io.Writer
  • encoding/csv.Reader/Writer 分別實現了 io.Reader 和 io.Writer
  • mime/multipart.Part 實現了 io.Reader
  • net/conn 分別實現了 io.Reader 和 io.Writer(Conn接口定義了Read/Write)

除此之外,io 包本身也有這兩個接口的實現類型。如:

實現了 Reader 的類型:LimitedReader、PipeReader、SectionReader
實現了 Writer 的類型:PipeWriter

以上類型中,常用的類型有:os.File、strings.Reader、bufio.Reader/Writer、bytes.Buffer、bytes.Reader

小貼士

從接口名稱很容易猜到,一般地, Go 中接口的命名約定:接口名以 er 結尾。注意,這里並非強行要求,你完全可以不以 er 結尾。標准庫中有些接口也不是以 er 結尾的。

1.6. ReaderAt 和 WriterAt 接口

ReaderAt 接口的定義如下:

type ReaderAt interface {
    ReadAt(p []byte, off int64) (n int, err error)
}

官方文檔中關於該接口方法的說明:

ReadAt 從基本輸入源的偏移量 off 處開始,將 len(p) 個字節讀取到 p 中。它返回讀取的字節數 n(0 <= n <= len(p))以及任何遇到的錯誤。

當 ReadAt 返回的 n < len(p) 時,它就會返回一個 非nil 的錯誤來解釋 為什么沒有返回更多的字節。在這一點上,ReadAt 比 Read 更嚴格。

即使 ReadAt 返回的 n < len(p),它也會在調用過程中使用 p 的全部作為暫存空間。若可讀取的數據不到 len(p) 字節,ReadAt 就會阻塞,直到所有數據都可用或一個錯誤發生。 在這一點上 ReadAt 不同於 Read。

若 n = len(p) 個字節從輸入源的結尾處由 ReadAt 返回,Read可能返回 err == EOF 或者 err == nil

若 ReadAt 攜帶一個偏移量從輸入源讀取,ReadAt 應當既不影響偏移量也不被它所影響。

可對相同的輸入源並行執行 ReadAt 調用。

可見,ReaderAt 接口使得可以從指定偏移量處開始讀取數據。

簡單示例代碼如下:

reader := strings.NewReader("Go語言中文網")
p := make([]byte, 6)
n, err := reader.ReadAt(p, 2)
if err != nil {
    panic(err)
}
fmt.Printf("%s, %d\n", p, n)

輸出:

語言, 6

WriterAt 接口的定義如下:

type WriterAt interface {
    WriteAt(p []byte, off int64) (n int, err error)
}

官方文檔中關於該接口方法的說明:

WriteAt 從 p 中將 len(p) 個字節寫入到偏移量 off 處的基本數據流中。它返回從 p 中被寫入的字節數 n(0 <= n <= len(p))以及任何遇到的引起寫入提前停止的錯誤。若 WriteAt 返回的 n < len(p),它就必須返回一個 非nil 的錯誤。

若 WriteAt 攜帶一個偏移量寫入到目標中,WriteAt 應當既不影響偏移量也不被它所影響。

若被寫區域沒有重疊,可對相同的目標並行執行 WriteAt 調用。

我們可以通過該接口將數據寫入到數據流的特定偏移量之后。

通過簡單示例來演示 WriteAt 方法的使用(os.File 實現了 WriterAt 接口):

file, err := os.Create("writeAt.txt")
if err != nil {
    panic(err)
}
defer file.Close()
file.WriteString("Golang中文社區——這里是多余")
n, err := file.WriteAt([]byte("Go語言中文網"), 24)
if err != nil {
    panic(err)
}
fmt.Println(n)

打開文件 WriteAt.txt,內容是:Golang中文社區——Go語言中文網

分析:

file.WriteString("Golang中文社區——這里是多余") 往文件中寫入 Golang中文社區——這里是多余,之后 file.WriteAt([]byte("Go語言中文網"), 24) 在文件流的 offset=24 處寫入 Go語言中文網(會覆蓋該位置的內容)。

1.7. ReaderFrom 和 WriterTo 接口

ReaderFrom 的定義如下:

type ReaderFrom interface {
    ReadFrom(r Reader) (n int64, err error)
}

官方文檔中關於該接口方法的說明:

ReadFrom 從 r 中讀取數據,直到 EOF 或發生錯誤。其返回值 n 為讀取的字節數。除 io.EOF 之外,在讀取過程中遇到的任何錯誤也將被返回。

如果 ReaderFrom 可用,Copy 函數就會使用它。

注意:ReadFrom 方法不會返回 err == EOF。

下面的例子簡單的實現將文件中的數據全部讀取(顯示在標准輸出):

file, err := os.Open("writeAt.txt")
if err != nil {
    panic(err)
}
defer file.Close()
writer := bufio.NewWriter(os.Stdout)
writer.ReadFrom(file)
writer.Flush()

當然,我們可以通過 ioutil 包的 ReadFile 函數獲取文件全部內容。其實,跟蹤一下 ioutil.ReadFile 的源碼,會發現其實也是通過 ReadFrom 方法實現(用的是 bytes.Buffer,它實現了 ReaderFrom 接口)。

如果不通過 ReadFrom 接口來做這件事,而是使用 io.Reader 接口,我們有兩種思路:

  1. 先獲取文件的大小(File 的 Stat 方法),之后定義一個該大小的 []byte,通過 Read 一次性讀取
  2. 定義一個小的 []byte,不斷的調用 Read 方法直到遇到 EOF,將所有讀取到的 []byte 連接到一起

這里不給出實現代碼了,有興趣的可以實現一下。

提示

通過查看 bufio.Writer 或 strings.Buffer 類型的 ReadFrom 方法實現,會發現,其實它們的實現和上面說的第 2 種思路類似。

WriterTo的定義如下:

type WriterTo interface {
    WriteTo(w Writer) (n int64, err error)
}

官方文檔中關於該接口方法的說明:

WriteTo 將數據寫入 w 中,直到沒有數據可寫或發生錯誤。其返回值 n 為寫入的字節數。 在寫入過程中遇到的任何錯誤也將被返回。

如果 WriterTo 可用,Copy 函數就會使用它。

讀者是否發現,其實 ReaderFrom 和 WriterTo 接口的方法接收的參數是 io.Reader 和 io.Writer 類型。根據 io.Reader 和 io.Writer 接口的講解,對該接口的使用應該可以很好的掌握。

這里只提供簡單的一個示例代碼:將一段文本輸出到標准輸出

reader := bytes.NewReader([]byte("Go語言中文網"))
reader.WriteTo(os.Stdout)

通過 io.ReaderFrom 和 io.WriterTo 的學習,我們知道,如果這樣的需求,可以考慮使用這兩個接口:“一次性從某個地方讀或寫到某個地方去。”

1.8. Seeker 接口

接口定義如下:

type Seeker interface {
    Seek(offset int64, whence int) (ret int64, err error)
}

官方文檔中關於該接口方法的說明:

Seek 設置下一次 Read 或 Write 的偏移量為 offset,它的解釋取決於 whence: 0 表示相對於文件的起始處,1 表示相對於當前的偏移,而 2 表示相對於其結尾處。 Seek 返回新的偏移量和一個錯誤,如果有的話。

也就是說,Seek 方法是用於設置偏移量的,這樣可以從某個特定位置開始操作數據流。聽起來和 ReaderAt/WriteAt 接口有些類似,不過 Seeker 接口更靈活,可以更好的控制讀寫數據流的位置。

簡單的示例代碼:獲取倒數第二個字符(需要考慮 UTF-8 編碼,這里的代碼只是一個示例)

reader := strings.NewReader("Go語言中文網") reader.Seek(-6, io.SeekEnd) r, _, _ := reader.ReadRune() fmt.Printf("%c\n", r) 

小貼士

whence 的值,在 io 包中定義了相應的常量,應該使用這些常量

const ( SeekStart = 0 // seek relative to the origin of the file SeekCurrent = 1 // seek relative to the current offset SeekEnd = 2 // seek relative to the end ) 

而原先 os 包中的常量已經被標注為Deprecated

// Deprecated: Use io.SeekStart, io.SeekCurrent, and io.SeekEnd. const ( SEEK_SET int = 0 // seek relative to the origin of the file SEEK_CUR int = 1 // seek relative to the current offset SEEK_END int = 2 // seek relative to the end ) 

1.9. Closer接口

接口定義如下:

type Closer interface { Close() error } 

該接口比較簡單,只有一個 Close() 方法,用於關閉數據流。

文件 (os.File)、歸檔(壓縮包)、數據庫連接、Socket 等需要手動關閉的資源都實現了 Closer 接口。

實際編程中,經常將 Close 方法的調用放在 defer 語句中。

小提示

初學者容易寫出這樣的代碼:

file, err := os.Open("studygolang.txt") defer file.Close() if err != nil { ... } 

當文件 studygolang.txt 不存在或找不到時,file.Close() 會 panic,因為 file 是 nil。因此,應該將 defer file.Close() 放在錯誤檢查之后。

經過 issue40 提醒,查看源碼:

func (f *File) Close() error { if f == nil { return ErrInvalid } return f.file.close() } 

可見並不會 panic,但在 Close 之前校驗錯誤是個好習慣!

1.10. 其他接口

1.10.1. ByteReader 和 ByteWriter

通過名稱大概也能猜出這組接口的用途:讀或寫一個字節。接口定義如下:

type ByteReader interface { ReadByte() (c byte, err error) } type ByteWriter interface { WriteByte(c byte) error } 

在標准庫中,有如下類型實現了 io.ByteReader 或 io.ByteWriter:

  • bufio.Reader/Writer 分別實現了io.ByteReader 和 io.ByteWriter
  • bytes.Buffer 同時實現了 io.ByteReader 和 io.ByteWriter
  • bytes.Reader 實現了 io.ByteReader
  • strings.Reader 實現了 io.ByteReader

接下來的示例中,我們通過 bytes.Buffer 來一次讀取或寫入一個字節(主要代碼):

var ch byte fmt.Scanf("%c\n", &ch) buffer := new(bytes.Buffer) err := buffer.WriteByte(ch) if err == nil { fmt.Println("寫入一個字節成功!准備讀取該字節……") newCh, _ := buffer.ReadByte() fmt.Printf("讀取的字節:%c\n", newCh) } else { fmt.Println("寫入錯誤") } 

程序從標准輸入接收一個字節(ASCII 字符),調用 buffer 的 WriteByte 將該字節寫入 buffer 中,之后通過 ReadByte 讀取該字節。完整的代碼見:code/src/chapter01/io/byterwer.go

一般地,我們不會使用 bytes.Buffer 來一次讀取或寫入一個字節。那么,這兩個接口有哪些用處呢?

在標准庫 encoding/binary 中,實現Google-ProtoBuf中的 Varints 讀取,ReadVarint 就需要一個 io.ByteReader 類型的參數,也就是說,它需要一個字節一個字節的讀取。關於 encoding/binary 包在后面會詳細介紹。

在標准庫 image/jpeg 中,Encode函數的內部實現使用了 ByteWriter 寫入一個字節。

小貼士

可以通過在 Go 語言源碼 src/pkg 中搜索 "io.ByteReader" 或 "io.ByteWiter",獲得哪些地方用到了這兩個接口。你會發現,這兩個接口在二進制數據或歸檔壓縮時用的比較多。

1.10.2. ByteScanner、RuneReader 和 RuneScanner

將這三個接口放在一起,是考慮到與 ByteReader 相關或相應。

ByteScanner 接口的定義如下:

type ByteScanner interface { ByteReader UnreadByte() error } 

可見,它內嵌了 ByteReader 接口(可以理解為繼承了 ByteReader 接口),UnreadByte 方法的意思是:將上一次 ReadByte 的字節還原,使得再次調用 ReadByte 返回的結果和上一次調用相同,也就是說,UnreadByte 是重置上一次的 ReadByte。注意,UnreadByte 調用之前必須調用了 ReadByte,且不能連續調用 UnreadByte。即:

buffer := bytes.NewBuffer([]byte{'a', 'b'}) err := buffer.UnreadByte() 

buffer := bytes.NewBuffer([]byte{'a', 'b'}) buffer.ReadByte() err := buffer.UnreadByte() err = buffer.UnreadByte() 

err 都 非nil,錯誤為:bytes.Buffer: UnreadByte: previous operation was not a read

RuneReader 接口和 ByteReader 類似,只是 ReadRune 方法讀取單個 UTF-8 字符,返回其 rune 和該字符占用的字節數。該接口在 regexp 包有用到。

之前有人在QQ群中問道:

strings.Index("行業交流群", "交流") 返回的是單字節字符的位置:6。但是想要的是 unicode 字符的位置:2。

這里借助utf8的RuneCountInString函數,實現代碼如下:

// strings.Index 的 UTF-8 版本 // 即 Utf8Index("Go語言中文網", "中文") 返回 4,而不是 strings.Index 的 8 func Utf8Index(str, substr string) int { index := strings.Index(str, substr) if index < 0{ return -1 } return utf8.RuneCountInString(str[:index]) } 

RuneScanner 接口和 ByteScanner 類似,就不贅述了。

1.10.3. ReadCloser、ReadSeeker、ReadWriteCloser、ReadWriteSeeker、ReadWriter、WriteCloser 和 WriteSeeker 接口

這些接口是上面介紹的接口的兩個或三個組合而成的新接口。例如 ReadWriter 接口:

type ReadWriter interface { Reader Writer } 

這是 Reader 接口和 Writer 接口的簡單組合(內嵌)。

這些接口的作用是:有些時候同時需要某兩個接口的所有功能,即必須同時實現了某兩個接口的類型才能夠被傳入使用。可見,io 包中有大量的“小接口”,這樣方便組合為“大接口”。

1.11. SectionReader 類型

SectionReader 是一個 struct(沒有任何導出的字段),實現了 Read, Seek 和 ReadAt,同時,內嵌了 ReaderAt 接口。結構定義如下:

type SectionReader struct { r ReaderAt // 該類型最終的 Read/ReadAt 最終都是通過 r 的 ReadAt 實現 base int64 // NewSectionReader 會將 base 設置為 off off int64 // 從 r 中的 off 偏移處開始讀取數據 limit int64 // limit - off = SectionReader 流的長度 } 

從名稱我們可以猜到,該類型讀取數據流中部分數據。看一下

func NewSectionReader(r ReaderAt, off int64, n int64) *SectionReader 

的文檔說明就知道了:

NewSectionReader 返回一個 SectionReader,它從 r 中的偏移量 off 處讀取 n 個字節后以 EOF 停止。

也就是說,SectionReader 只是內部(內嵌)ReaderAt 表示的數據流的一部分:從 off 開始后的 n 個字節。

這個類型的作用是:方便重復操作某一段 (section) 數據流;或者同時需要 ReadAt 和 Seek 的功能。

由於該類型所支持的操作,前面都有介紹,因此不提供示例代碼了。

關於該類型在標准庫中的使用,我們在 8.5 archive/zip — zip歸檔訪問 會講到。

1.12. LimitedReader 類型

LimitedReader 結構定義如下:

type LimitedReader struct { R Reader // underlying reader,最終的讀取操作通過 R.Read 完成 N int64 // max bytes remaining } 

文檔說明如下:

從 R 讀取但將返回的數據量限制為 N 字節。每調用一次 Read 都將更新 N 來反應新的剩余數量。

也就是說,最多只能返回 N 字節數據。

LimitedReader 只實現了 Read 方法(Reader 接口)。

使用示例如下:

content := "This Is LimitReader Example" reader := strings.NewReader(content) limitReader := &io.LimitedReader{R: reader, N: 8} for limitReader.N > 0 { tmp := make([]byte, 2) limitReader.Read(tmp) fmt.Printf("%s", tmp) } 

輸出:

This Is

可見,通過該類型可以達到 只允許讀取一定長度數據 的目的。

在 io 包中,LimitReader 函數的實現其實就是調用 LimitedReader:

func LimitReader(r Reader, n int64) Reader { return &LimitedReader{r, n} } 

1.13. PipeReader 和 PipeWriter 類型

PipeReader(一個沒有任何導出字段的 struct)是管道的讀取端。它實現了 io.Reader 和 io.Closer 接口。結構定義如下:

type PipeReader struct { p *pipe } 

關於 PipeReader.Read 方法的說明:從管道中讀取數據。該方法會堵塞,直到管道寫入端開始寫入數據或寫入端被關閉。如果寫入端關閉時帶有 error(即調用 CloseWithError 關閉),該Read返回的 err 就是寫入端傳遞的error;否則 err 為 EOF。

PipeWriter(一個沒有任何導出字段的 struct)是管道的寫入端。它實現了 io.Writer 和 io.Closer 接口。結構定義如下:

type PipeWriter struct { p *pipe } 

關於 PipeWriter.Write 方法的說明:寫數據到管道中。該方法會堵塞,直到管道讀取端讀完所有數據或讀取端被關閉。如果讀取端關閉時帶有 error(即調用 CloseWithError 關閉),該Write返回的 err 就是讀取端傳遞的error;否則 err 為 ErrClosedPipe。

使用示例如下:

func main() { pipeReader, pipeWriter := io.Pipe() go PipeWrite(pipeWriter) go PipeRead(pipeReader) time.Sleep(30 * time.Second) } func PipeWrite(writer *io.PipeWriter){ data := []byte("Go語言中文網") for i := 0; i < 3; i++{ n, err := writer.Write(data) if err != nil{ fmt.Println(err) return } fmt.Printf("寫入字節 %d\n",n) } writer.CloseWithError(errors.New("寫入段已關閉")) } func PipeRead(reader *io.PipeReader){ buf := make([]byte, 128) for{ fmt.Println("接口端開始阻塞5秒鍾...") time.Sleep(5 * time.Second) fmt.Println("接收端開始接受") n, err := reader.Read(buf) if err != nil{ fmt.Println(err) return } fmt.Printf("收到字節: %d\n buf內容: %s\n",n,buf) } } 

io.Pipe() 用於創建一個同步的內存管道 (synchronous in-memory pipe),函數簽名:

func Pipe() (*PipeReader, *PipeWriter) 

它將 io.Reader 連接到 io.Writer。一端的讀取匹配另一端的寫入,直接在這兩端之間復制數據;它沒有內部緩存。它對於並行調用 Read 和 Write 以及其它函數或 Close 來說都是安全的。一旦等待的 I/O 結束,Close 就會完成。並行調用 Read 或並行調用 Write 也同樣安全:同種類的調用將按順序進行控制。

正因為是同步的,因此不能在一個 goroutine 中進行讀和寫。

另外,對於管道的 close 方法(非 CloseWithError 時),err 會被置為 EOF。

1.14. Copy 和 CopyN 函數

Copy 函數的簽名:

func Copy(dst Writer, src Reader) (written int64, err error) 

函數文檔:

Copy 將 src 復制到 dst,直到在 src 上到達 EOF 或發生錯誤。它返回復制的字節數,如果有錯誤的話,還會返回在復制時遇到的第一個錯誤。

成功的 Copy 返回 err == nil,而非 err == EOF。由於 Copy 被定義為從 src 讀取直到 EOF 為止,因此它不會將來自 Read 的 EOF 當做錯誤來報告。

若 dst 實現了 ReaderFrom 接口,其復制操作可通過調用 dst.ReadFrom(src) 實現。此外,若 src 實現了 WriterTo 接口,其復制操作可通過調用 src.WriteTo(dst) 實現。

代碼:

io.Copy(os.Stdout, strings.NewReader("Go語言中文網")) 

直接將內容輸出(寫入 Stdout 中)。

我們甚至可以這么做:

package main import ( "fmt" "io" "os" ) func main() { io.Copy(os.Stdout, os.Stdin) fmt.Println("Got EOF -- bye") } 

執行:echo "Hello, World" | go run main.go

CopyN 函數的簽名:

func CopyN(dst Writer, src Reader, n int64) (written int64, err error) 

函數文檔:

CopyN 將 n 個字節(或到一個error)從 src 復制到 dst。 它返回復制的字節數以及在復制時遇到的最早的錯誤。當且僅當err == nil時,written == n 。

若 dst 實現了 ReaderFrom 接口,復制操作也就會使用它來實現。

代碼:

io.CopyN(os.Stdout, strings.NewReader("Go語言中文網"), 8) 

會輸出:

Go語言

1.15. ReadAtLeast 和 ReadFull 函數

ReadAtLeast 函數的簽名:

func ReadAtLeast(r Reader, buf []byte, min int) (n int, err error) 

函數文檔:

ReadAtLeast 將 r 讀取到 buf 中,直到讀了最少 min 個字節為止。它返回復制的字節數,如果讀取的字節較少,還會返回一個錯誤。若沒有讀取到字節,錯誤就只是 EOF。如果一個 EOF 發生在讀取了少於 min 個字節之后,ReadAtLeast 就會返回 ErrUnexpectedEOF。若 min 大於 buf 的長度,ReadAtLeast 就會返回 ErrShortBuffer。對於返回值,當且僅當 err == nil 時,才有 n >= min。

一般可能不太會用到這個函數。使用時需要注意返回的 error 判斷。

ReadFull 函數的簽名:

func ReadFull(r Reader, buf []byte) (n int, err error) 

函數文檔:

ReadFull 精確地從 r 中將 len(buf) 個字節讀取到 buf 中。它返回復制的字節數,如果讀取的字節較少,還會返回一個錯誤。若沒有讀取到字節,錯誤就只是 EOF。如果一個 EOF 發生在讀取了一些但不是所有的字節后,ReadFull 就會返回 ErrUnexpectedEOF。對於返回值,當且僅當 err == nil 時,才有 n == len(buf)。

注意該函數和 ReadAtLeast 的區別:ReadFull 將 buf 讀滿;而 ReadAtLeast 是最少讀取 min 個字節。

1.16. WriteString 函數

這是為了方便寫入 string 類型提供的函數,函數簽名:

func WriteString(w Writer, s string) (n int, err error) 

函數文檔:

WriteString 將s的內容寫入w中,當 w 實現了 WriteString 方法時,會直接調用該方法,否則執行 w.Write([]byte(s))。

1.17. MultiReader 和 MultiWriter 函數

這兩個函數的定義分別是:

func MultiReader(readers ...Reader) Reader func MultiWriter(writers ...Writer) Writer 

它們接收多個 Reader 或 Writer,返回一個 Reader 或 Writer。我們可以猜想到這兩個函數就是操作多個 Reader 或 Writer 就像操作一個。

事實上,在 io 包中定義了兩個非導出類型:mutilReader 和 multiWriter,它們分別實現了 io.Reader 和 io.Writer 接口。類型定義為:

type multiReader struct { readers []Reader } type multiWriter struct { writers []Writer } 

對於這兩種類型對應的實現方法(Read 和 Write 方法)的使用,我們通過例子來演示。

MultiReader 的使用:

readers := []io.Reader{
    strings.NewReader("from strings reader"), bytes.NewBufferString("from bytes buffer"), } reader := io.MultiReader(readers...) data := make([]byte, 0, 128) buf := make([]byte, 10) for n, err := reader.Read(buf); err != io.EOF ; n, err = reader.Read(buf){ if err != nil{ panic(err) } data = append(data,buf[:n]...) } fmt.Printf("%s\n", data) 

輸出:

from strings readerfrom bytes buffer

代碼中首先構造了一個 io.Reader 的 slice,由 strings.Reader 和 bytes.Buffer 兩個實例組成,然后通過 MultiReader 得到新的 Reader,循環讀取新 Reader 中的內容。從輸出結果可以看到,第一次調用 Reader 的 Read 方法獲取到的是 slice 中第一個元素的內容……也就是說,MultiReader 只是邏輯上將多個 Reader 組合起來,並不能通過調用一次 Read 方法獲取所有 Reader 的內容。在所有的 Reader 內容都被讀完后,Reader 會返回 EOF。

MultiWriter 的使用:

file, err := os.Create("tmp.txt") if err != nil { panic(err) } defer file.Close() writers := []io.Writer{ file, os.Stdout, } writer := io.MultiWriter(writers...) writer.Write([]byte("Go語言中文網")) 

這段程序執行后在生成 tmp.txt 文件,同時在文件和屏幕中都輸出:Go語言中文網。這和 Unix 中的 tee 命令類似。

動手試試

Go 實現 Unix 中 tee 命令的功能很簡單吧。MultiWriter 的 Write 方法是如何實現的?有興趣可以自己實現一個,然后對着源碼比較一下。

1.18. TeeReader函數

函數簽名如下:

func TeeReader(r Reader, w Writer) Reader 

TeeReader 返回一個 Reader,它將從 r 中讀到的數據寫入 w 中。所有經由它處理的從 r 的讀取都匹配於對應的對 w 的寫入。它沒有內部緩存,即寫入必須在讀取完成前完成。任何在寫入時遇到的錯誤都將作為讀取錯誤返回。

也就是說,我們通過 Reader 讀取內容后,會自動寫入到 Writer 中去。例子代碼如下:

reader := io.TeeReader(strings.NewReader("Go語言中文網"), os.Stdout) reader.Read(make([]byte, 20)) 

輸出結果:

Go語言中文網

這種功能的實現其實挺簡單,無非是在 Read 完后執行 Write。

至此,io 所有接口、類型和函數都講解完成。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM