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所有介面、型別和函式都講解完成。