1. 程式人生 > >golang中io包用法(二)

golang中io包用法(二)



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

由於這些介面和原語以不同的實現包裝了低階操作,因此除非另行通知,否則客戶端不應假定它們對於並行執行是安全的。

在io包中最重要的是兩個介面:Reader和Writer介面。本章所提到的各種IO包,都跟這兩個介面有關,也就是說,只要實現了這兩個介面,它就有了IO的功能。

 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),它也會在呼叫過程中使用 p 的全部作為暫存空間。若一些資料可用但不到 len(p) 個位元組,Read 會照例返回可用的資料,而不是等待更多資料。

//當 Read 在成功讀取 n > 0 個位元組後遇到一個錯誤或EOF(end-of-file),它就會返回讀取的位元組數。它會從相同的呼叫中返回(非nil的)錯誤或從隨後的呼叫中返回錯誤(同時 n == 0)。 一般情況的一個例子就是 Reader 在輸入流結束時會返回一個非零的位元組數,同時返回的err不是EOF就是nil。無論如何,下一個 Read 都應當返回 0, EOF。

//呼叫者應當總在考慮到錯誤 err 前處理 n > 0 的位元組。這樣做可以在讀取一些位元組,以及允許的 EOF 行為後正確地處理I/O錯誤。

也就是說,當Read方法返回錯誤時,不代表沒有讀取到任何資料。呼叫者應該處理返回的任何資料,之後才處理可能的錯誤。

根據Go語言中關於介面和實現了介面的型別的定義,我們知道Reader介面的方法集只包含一個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)


**小貼士**

io.EOF 變數的定義:`var EOF = errors.New("EOF")`,是error型別。根據reader介面的說明,在 n > 0 且資料被讀完了的情況下,返回的error有可能是EOF也有可能是nil。


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

實現了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介面的型別(匯出的型別)

- 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</span>

除此之外,io包本身也有這兩個介面的實現型別。如:

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

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


**小貼士**

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

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 返回,那麼這時 err == EOF 或者 err == nil。

> 若 ReadAt 按查詢偏移量從輸入源讀取,ReadAt 應當既不影響基本查詢偏移量也不被它所影響。

> ReadAt 的客戶端可對相同的輸入源並行執行 ReadAt 呼叫。


可見,ReaderAt介面使得可以從指定偏移量處開始讀取資料。

簡單示例程式碼如下:
package main

import (
	"fmt"
	"strings"
)

func main() {
	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 呼叫。


我們可以通過該介面將資料寫入資料流的特定偏移量之後。

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

package main

import (
	"fmt"
	"os"
)

func main() {
	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語言學習園地`(會覆蓋該位置的內容)。

ReaderFrom 和 WriterTo 介面

**ReaderFrom**的定義如下:

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

官方文件中關於該介面方法的說明:

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

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

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

下面的例子簡單的實現將檔案中的資料全部讀取(顯示在標準輸出):
package main

import (
	"bufio"
	"os"
)

func main() {

	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連線到一起

程式碼實現如下:
方法1:
package main

import (
	"fmt"
	"os"
)

func main() {

	file, err := os.Open("writeAt.txt")
	if err != nil {
		panic(err)
	}
	defer file.Close()
	stat, err := file.Stat()
	if err != nil {
		fmt.Println(err)
	}
	size := stat.Size()

	a := make([]byte, size)
	file.Read(a)
	fmt.Println(string(a))
}
執行結果: Golang中文社群——Go語言學習園地

方法2:
package main

import (
    "fmt"
    "os"
)

func main() {

    file, err := os.Open("writeAt.txt")
    if err != nil {
        panic(err)
    }
    defer file.Close()
    a := make([]byte, 5)
    var b []byte
    for n, err := file.Read(a); err == nil; n, err = file.Read(a) {
        b = append(b, a[:n]...)
    }
    fmt.Println(string(b))
}
執行結果: Golang中文社群——Go語言學習園地

**提示**通過檢視 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的學習,我們知道,如果這樣的需求,可以考慮使用這兩個介面:“一次性從某個地方讀或寫到某個地方去。”

## 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編碼,這裡的程式碼只是一個示例)

package main

import (
	"fmt"
	"strings"
)

func main() {

	reader := strings.NewReader("Go語言學習園地")
	reader.Seek(-6, 2)
	r, _, _ := reader.ReadRune()
	fmt.Printf("%c\n", r)
}
執行結果:園 


**小貼士**

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

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
)

## 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()放在錯誤檢查之後。

其他介面

 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</span>


接下來的示例中,我們通過bytes.Buffer來一次讀取或寫入一個位元組(主要程式碼):
package main

import (
	"bytes"
	"fmt"
)

func main() {
	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讀取該位元組。

一般地,我們不會使用bytes.Buffer來一次讀取或寫入一個位元組。那麼,這兩個介面有哪些用處呢?

在標準庫encoding/binary中,實現Varints讀取,就需要一個io.ByteReader型別的引數,也就是說,它需要一個位元組一個位元組的讀取。關於encoding/binary包在後面會詳細介紹。

在標準庫image/jpeg中,Encode函式的內部實現使用了ByteWriter寫入一個位元組。

**小貼士**

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



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



package main

import (
	"bytes"
	"fmt"
)

func main() {

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

	err := buffer.UnreadByte()
	fmt.Println(err) //<nil>
	err = buffer.UnreadByte()
	fmt.Println(err) //  bytes.Buffer: UnreadByte: previous operation was not a read

}

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

RuneReader介面和ByteReader類似,只是ReadRune方法讀取單個UTF-8字元,返回其rune和該字元佔用的位元組數。該介面在regexp包有用到。


RuneScanner介面和ByteScanner類似,就不贅述了。



ReadCloser、ReadSeeker、ReadWriteCloser、ReadWriteSeeker、ReadWriter、WriteCloser和WriteSeeker介面


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

type ReadWriter interface {
	Reader
	Writer
}


這是Reader介面和Writer介面的簡單組合(內嵌)。

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

## 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的功能。

由於該型別所支援的操作,前面都有介紹,因此提供示例程式碼了。

關於該型別在標準庫中的使用,我們在 [archive/zip — zip歸檔訪問]() 會講到。

## LimitedReader 型別 ##

LimitedReader 型別定義如下:

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

文件說明如下:

> 從 R 讀取但將返回的資料量限制為 N 位元組。每呼叫一次 Read 都將更新 N 來反應新的剩餘數量。

也就是說,最多隻能返回 N 位元組資料。

LimitedReader只實現了Read方法(Reader介面)。

使用示例如下:
package main

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

func main() {

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

}
執行結果:This Is LimitReader

可見,通過該型別可以達到 *只允許讀取一定長度資料* 的目的。

在io包中,LimitReader 函式的實現其實就是呼叫 LimitedReader:

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



PipReader 和 PipWriter 型別

PipReader(一個沒有任何匯出欄位的struct)是管道的讀取端。它實現了io.Reader和io.Closer介面。

**關於 Read 方法的說明**:從管道中讀取資料。該方法會堵塞,直到管道寫入端開始寫入資料或寫入端關閉了。如果寫入端關閉時帶上了error(即呼叫CloseWithError關閉),該方法返回的err就是寫入端傳遞的error;否則err為EOF。

PipWriter(一個沒有任何匯出欄位的struct)是管道的寫入端。它實現了io.Writer和io.Closer介面。

**關於 Write 方法的說明**:寫資料到管道中。該方法會堵塞,直到管道讀取端讀完所有資料或讀取端關閉了。如果讀取端關閉時帶上了error(即呼叫CloseWithError關閉),該方法返回的err就是讀取端傳遞的error;否則err為 ErrClosedPipe。

其他方法的使用通過例子一起講解:

package main

import (
	"errors"
	"fmt"
	"io"
	"time"
)

func main() {
	Pipe()
}

func Pipe() {
	pipeReader, pipeWriter := io.Pipe()
	go PipeRead(pipeReader)
	PipeWrite(pipeWriter)
	time.Sleep(1e7)
}

func PipeWrite(pipeWriter *io.PipeWriter) {
	var (
		i   = 0
		err error
		n   int
	)
	data := []byte("Go語言學習園地")
	for _, err = pipeWriter.Write(data); err == nil; n, err = pipeWriter.Write(data) {
		i++
		if i == 3 {
			pipeWriter.CloseWithError(errors.New("輸出3次後結束"))
		}
	}
	fmt.Println("close 後輸出的位元組數:", n, " error:", err)
}

func PipeRead(pipeReader *io.PipeReader) {
	var (
		err error
		n   int
	)
	data := make([]byte, 1024)
	for n, err = pipeReader.Read(data); err == nil; n, err = pipeReader.Read(data) {
		fmt.Printf("%s\n", data[:n])
	}
	fmt.Println("writer 端 closewitherror後:", err)
}


執行結果:
 Go語言學習園地
  Go語言學習園地
  Go語言學習園地
  close 後輸出的位元組數: 0  error: io: read/write on closed pipe
  writer 端 closewitherror後: 輸出3次後結束

程式碼分析:
io.Pipe()用於建立建立一個同步的記憶體管道(synchronous in-memory pipe),函式簽名:

func Pipe() (*PipeReader, *PipeWriter)


它將 io.Reader 連線到 io.Writer。一端的讀取匹配另一端的寫入,直接在這兩端之間複製資料;它沒有內部快取。它對於並行呼叫 Read 和 Write 以及其它函式或 Close 來說都是安全的。 一旦等待的I/O結束,Close 就會完成。並行呼叫 Read 或並行呼叫 Write 也同樣安全: 同種類的呼叫將按順序進行控制。稍候我們會分析管道相關的原始碼。

正因為是同步的(其原理和無快取channel類似),因此不能在一個goroutine中進行讀和寫。


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) 實現。此外,若 dst 實現了 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 個位元組從 src 複製到 dst。 它返回複製的位元組數以及在複製時遇到的最早的錯誤。由於 Read 可以返回要求的全部數量及一個錯誤(包括 EOF),因此 CopyN 也能如此。

> 若 dst 實現了 ReaderFrom 介面,複製操作也就會使用它來實現。
<span style="color:#FF0000;">注:只有當written=n時,返回的err才為nil,否則都不為nil</span>

程式碼:

io.CopyN(os.Stdout, strings.NewReader("Go語言學習園地"), 8)

會輸出:

Go語言



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個位元組。

WriteString 函式

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

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

當 w 實現了 WriteString 方法時,直接呼叫該方法,否則執行w.Write([]byte(s))。



 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的使用**:
package main

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

func main() {

	readers := []io.Reader{
		strings.NewReader("from strings reader"),
		bytes.NewBufferString("from bytes buffer"),
	}
	reader := io.MultiReader(readers...)
	data := make([]byte, 0, 1024)
	var (
		err error
		n   int
	)
	for err != io.EOF {
		tmp := make([]byte, 512)
		n, err = reader.Read(tmp)
		if err == nil {
			data = append(data, tmp[:n]...)
		} else {
			if err != io.EOF {
				panic(err)
			}
		}
	}
	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的使用**:
package main

import (
	"io"
	"os"
)

func main() {

	file, err := os.Create("tmp.txt")
	if err != nil {
		panic(err)
	}
	defer file.Close()
	writers := []io.Writer{
		os.Stdout,
		file,
	}
	writer := io.MultiWriter(writers...)
	writer.Write([]byte("hello,world"))
}


這段程式執行後在生成tmp.txt檔案,同時在檔案和螢幕中都輸出:hello world。


 TeeReader函式

函式簽名如下:

func TeeReader(r Reader, w Writer) Reader

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

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

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

func main() {
	reader := io.TeeReader(strings.NewReader("Go語言學習園地"), os.Stdout)
	p := make([]byte, 20)
	n, err := reader.Read(p)
	fmt.Println(string(p), n, err)
}


輸出結果:

 Go語言學習園地Go語言學習園地 20 <nil>

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

至此,io所有介面、型別和函式都講解完成。