Go標(biāo)準(zhǔn)庫-io

?一般的都伪,計(jì)算機(jī)程序是:輸入 (Input) 經(jīng)過算法處理產(chǎn)生輸出 (Output)。各種語言一般都會(huì)提供IO庫供開發(fā)者使用。Go語言也不例外让网。
?Go 語言中,為了方便開發(fā)者使用型将,將 IO 操作封裝在了如下幾個(gè)包中:
-io 為 IO 原語(I/O primitives)提供基本的接口
-io/ioutil 封裝一些實(shí)用的 I/O 函數(shù)
-fmt 實(shí)現(xiàn)格式化 I/O寂祥,類似 C 語言中的 printf 和 scanf
-bufio 實(shí)現(xiàn)帶緩沖I/O
?這里會(huì)詳細(xì)介紹這些 IO 包提供的函數(shù)、類型和方法七兜,同時(shí)通過實(shí)例講解這些包的使用方法。

基本的 IO 接口

?io 包為 I/O 原語提供了基本的接口福扬。它主要包裝了這些原語的已有實(shí)現(xiàn)腕铸。由于這些被接口包裝的I/O原語是由不同的低級(jí)操作實(shí)現(xiàn),因此铛碑,在另有聲明之前不該假定它們的并行執(zhí)行是安全的狠裹。

?在 io 包中最重要的兩個(gè)接口是io.go文件中:Reader 和 Writer 接口。本章所提到的各種 IO 包汽烦,都跟這兩個(gè)接口有關(guān)涛菠,也就是說,只要滿足這兩個(gè)接口,它就可以使用 IO 包的功能俗冻。

Reader接口:

/*
Read 將 len(p) 個(gè)字節(jié)讀取到 p 中礁叔。它返回讀取的字節(jié)數(shù) n(0 <= n <= len(p)) 以及任何遇到的錯(cuò)誤。
即使 Read 返回的 n < len(p)迄薄,它也會(huì)在調(diào)用過程中占用 len(p) 個(gè)字節(jié)作為暫存空間琅关。若可讀取的數(shù)據(jù)不到 len(p) 個(gè)字節(jié),
Read 會(huì)返回可用數(shù)據(jù)讥蔽,而不是等待更多數(shù)據(jù)涣易。
當(dāng) Read 在成功讀取 n > 0 個(gè)字節(jié)后遇到一個(gè)錯(cuò)誤或 EOF (end-of-file),它會(huì)返回讀取的字節(jié)數(shù)冶伞。
它可能會(huì)同時(shí)在本次的調(diào)用中返回一個(gè)non-nil錯(cuò)誤,或在下一次的調(diào)用中返回這個(gè)錯(cuò)誤(且 n 為 0)新症。 
一般情況下, Reader會(huì)返回一個(gè)非0字節(jié)數(shù)n, 若 n = len(p) 個(gè)字節(jié)從輸入源的結(jié)尾處由 Read 返回,Read可能返回 err == EOF 或者 err == nil响禽。并且之后的 Read() 都應(yīng)該返回 (n:0, err:EOF)账劲。
調(diào)用者在考慮錯(cuò)誤之前應(yīng)當(dāng)首先處理返回的數(shù)據(jù)。這樣做可以正確地處理在讀取一些字節(jié)后產(chǎn)生的 I/O 錯(cuò)誤金抡,同時(shí)允許EOF的出現(xiàn)瀑焦。
根據(jù) Go 語言中關(guān)于接口和滿足了接口的類型的定義,我們知道 Reader 接口的方法集只包含一個(gè) Read 方法,因此梗肝,所有實(shí)現(xiàn)了 Read 方法的類型都滿足 io.Reader 接口榛瓮,也就是說,在所有需要 io.Reader 的地方巫击,可以傳遞實(shí)現(xiàn)了 Read() 方法的類型的實(shí)例禀晓。
*/
type Reader interface {
    Read(p []byte) (n int, err error)
}

下面,我們通過具體例子來談?wù)勗摻涌诘挠梅ā?/p>

//ReadFrom 函數(shù)將 io.Reader 作為參數(shù)坝锰,也就是說粹懒,ReadFrom 可以從任意的地方讀取數(shù)據(jù),只要來源實(shí)現(xiàn)了 io.Reader 接口顷级。比如凫乖,我們可以從標(biāo)準(zhǔn)輸入、文件弓颈、字符串等讀取數(shù)據(jù)帽芽,示例代碼如下:
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
}
// 從標(biāo)準(zhǔn)輸入讀取
data, err = ReadFrom(os.Stdin, 11)
// 從普通文件讀取,其中 file 是 os.File 的實(shí)例
data, err = ReadFrom(file, 9)
// 從字符串讀取
data, err = ReadFrom(strings.NewReader("from string"), 12)

小貼士
?io.EOF 變量的定義:var EOF = errors.New("EOF")翔冀,是 error 類型导街。根據(jù) reader 接口的說明,在 n > 0 且數(shù)據(jù)被讀完了的情況下纤子,當(dāng)次返回的 error 有可能是 EOF 也有可能是 nil搬瑰。

Writer 接口

/*
Write 將 len(p) 個(gè)字節(jié)從 p 中寫入到基本數(shù)據(jù)流中款票。
它返回從 p 中被寫入的字節(jié)數(shù) n(0 <= n <= len(p))以及任何遇到的引起寫入提前停止的錯(cuò)誤。
若 Write 返回的 n < len(p)泽论,它就必須返回一個(gè) 非nil 的錯(cuò)誤艾少。
*/
type Writer interface {
    Write(p []byte) (n int, err error)
}

?同樣的,所有實(shí)現(xiàn)了Write方法的類型都實(shí)現(xiàn)了 io.Writer 接口佩厚。
?在上個(gè)例子中姆钉,我們是自己實(shí)現(xiàn)一個(gè)函數(shù)接收一個(gè) io.Reader 類型的參數(shù)。這里抄瓦,我們通過標(biāo)準(zhǔn)庫的例子來學(xué)習(xí)潮瓶。
?在fmt標(biāo)準(zhǔn)庫print.go文件中,有一組函數(shù):Fprint/Fprintf/Fprintln钙姊,它們接收一個(gè) io.Wrtier 類型參數(shù)(第一個(gè)參數(shù))毯辅,也就是說它們將數(shù)據(jù)格式化輸出到 io.Writer 中。那么煞额,調(diào)用這組函數(shù)時(shí)思恐,該如何傳遞這個(gè)參數(shù)呢?
?我們以 fmt.Fprintln 為例膊毁,同時(shí)看一下 fmt.Println 函數(shù)的源碼胀莹。

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

?很顯然,fmt.Println會(huì)將內(nèi)容輸出到標(biāo)準(zhǔn)輸出中

實(shí)現(xiàn)了 io.Reader 接口或 io.Writer 接口的類型

?初學(xué)者看到函數(shù)參數(shù)是一個(gè)接口類型婚温,很多時(shí)候有些束手無策描焰,不知道該怎么傳遞參數(shù)。還有人問:標(biāo)準(zhǔn)庫中有哪些類型實(shí)現(xiàn)了 io.Reader 或 io.Writer 接口栅螟?
?通過本文上面的例子荆秦,我們可以知道,os.File 同時(shí)實(shí)現(xiàn)了這兩個(gè)接口力图。我們還看到 os.Stdin/Stdout 這樣的代碼步绸,它們似乎分別實(shí)現(xiàn)了 io.Reader/io.Writer 接口。沒錯(cuò)吃媒,實(shí)際上在 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 只是三個(gè)特殊的文件類型的標(biāo)識(shí)(即都是 os.File 的實(shí)例),自然也實(shí)現(xiàn)了 io.Reader 和 io.Writer晓折。
?列出實(shí)現(xiàn)了 io.Reader 或 io.Writer 接口的類型(導(dǎo)出的類型):(注:godoc 命令支持額外參數(shù) -analysis 惑朦,能列出都有哪些類型實(shí)現(xiàn)了某個(gè)接口,相關(guān)參考 godoc -h 漓概。
-os.File 同時(shí)實(shí)現(xiàn)了 io.Reader 和 io.Writer
-strings.Reader 實(shí)現(xiàn)了 io.Reader
-bufio.Reader/Writer 分別實(shí)現(xiàn)了 io.Reader 和 io.Writer
-bytes.Buffer 同時(shí)實(shí)現(xiàn)了 io.Reader 和 io.Writer
-bytes.Reader 實(shí)現(xiàn)了 io.Reader
-compress/gzip.Reader/Writer 分別實(shí)現(xiàn)了 io.Reader 和 io.Writer
-crypto/cipher.StreamReader/StreamWriter 分別實(shí)現(xiàn)了 io.Reader 和 io.Writer
-crypto/tls.Conn 同時(shí)實(shí)現(xiàn)了 io.Reader 和 io.Writer
-encoding/csv.Reader/Writer 分別實(shí)現(xiàn)了 io.Reader 和 io.Writer
-mime/multipart.Part 實(shí)現(xiàn)了 io.Reader
-net/conn 分別實(shí)現(xiàn)了 io.Reader 和 io.Writer(Conn接口定義了Read/Write)
除此之外,io 包本身也有這兩個(gè)接口的實(shí)現(xiàn)類型病梢。如:
實(shí)現(xiàn)了 Reader 的類型:LimitedReader胃珍、PipeReader梁肿、SectionReader
實(shí)現(xiàn)了 Writer 的類型:PipeWriter

ReaderAt 和 WriterAt 接口

/*
ReadAt 從基本輸入源的偏移量 off 處開始,將 len(p) 個(gè)字節(jié)讀取到 p 中觅彰。它返回讀取的字節(jié)數(shù) n(0 <= n <= len(p))以及任何遇到的錯(cuò)誤吩蔑。
當(dāng) ReadAt 返回的 n < len(p) 時(shí),它就會(huì)返回一個(gè) 非nil 的錯(cuò)誤來解釋 為什么沒有返回更多的字節(jié)填抬。在這一點(diǎn)上烛芬,ReadAt 比 Read 更嚴(yán)格。
即使 ReadAt 返回的 n < len(p)飒责,它也會(huì)在調(diào)用過程中使用 p 的全部作為暫存空間赘娄。若可讀取的數(shù)據(jù)不到 len(p) 字節(jié),ReadAt 就會(huì)阻塞,直到所有數(shù)據(jù)都可用或一個(gè)錯(cuò)誤發(fā)生宏蛉。 在這一點(diǎn)上 ReadAt 不同于 Read遣臼。
若 n = len(p) 個(gè)字節(jié)從輸入源的結(jié)尾處由 ReadAt 返回,Read可能返回 err == EOF 或者 err == nil
若 ReadAt 攜帶一個(gè)偏移量從輸入源讀取拾并,ReadAt 應(yīng)當(dāng)既不影響偏移量也不被它所影響揍堰。
可對(duì)相同的輸入源并行執(zhí)行 ReadAt 調(diào)用。
*/
type ReaderAt interface {
    ReadAt(p []byte, off int64) (n int, err error)
}

可見嗅义,ReaderAt 接口使得可以從指定偏移量處開始讀取數(shù)據(jù)屏歹。
簡(jiǎn)單示例代碼如下:

reader := strings.NewReader("一起學(xué)習(xí)Go語言")
p := make([]byte, 6)
n, err := reader.ReadAt(p, 3)
if err != nil {
    fmt.Println(err.Error())
}

fmt.Printf("%s, %d\n", p, n)
//輸出: 起學(xué), 6

WriterAt 接口的定義如下:

/*
WriteAt 從 p 中將 len(p) 個(gè)字節(jié)寫入到偏移量 off 處的基本數(shù)據(jù)流中。它返回從 p 中被寫入的字節(jié)數(shù) n(0 <= n <= len(p))以及任何遇到的引起寫入提前停止的錯(cuò)誤之碗。若 WriteAt 返回的 n < len(p)蝙眶,它就必須返回一個(gè) 非nil 的錯(cuò)誤。
若 WriteAt 攜帶一個(gè)偏移量寫入到目標(biāo)中继控,WriteAt 應(yīng)當(dāng)既不影響偏移量也不被它所影響械馆。
若被寫區(qū)域沒有重疊,可對(duì)相同的目標(biāo)并行執(zhí)行 WriteAt 調(diào)用武通。
*/
type WriterAt interface {
    WriteAt(p []byte, off int64) (n int, err error)
}

我們可以通過該接口將數(shù)據(jù)寫入到數(shù)據(jù)流的特定偏移量之后霹崎。
通過簡(jiǎn)單示例來演示 WriteAt 方法的使用(os.File 實(shí)現(xiàn)了 WriterAt 接口):

file, err := os.Create("write_at.txt")
    if err != nil {
        fmt.Println(err.Error())
        return
    }

    defer func() {
        err = file.Close()
        if err != nil {
            fmt.Println(err.Error())
        }
    }()

    _, err = file.WriteString("一起學(xué)習(xí)Go語言,")
    if err != nil {
        fmt.Println(err.Error())
        return
    }

    n, err := file.WriteAt([]byte("用我的方式"), 27)

    if err != nil {
        fmt.Println(err.Error())
        return
    }
    fmt.Println(n)
//會(huì)在當(dāng)前目錄生成write_at.txt文件, 打開文件 write_at.txt,內(nèi)容是:一起學(xué)習(xí)Go語言,用我的方式

ReaderFrom 和 WriterTo 接口

ReaderFrom 的定義如下:

/*
ReadFrom 從 r 中讀取數(shù)據(jù)冶忱,直到 EOF 或發(fā)生錯(cuò)誤尾菇。其返回值 n 為讀取的字節(jié)數(shù)。除 io.EOF 之外囚枪,在讀取過程中遇到的任何錯(cuò)誤也將被返回派诬。
如果 ReaderFrom 可用,Copy 函數(shù)就會(huì)使用它链沼。
*/
type ReaderFrom interface {
    ReadFrom(r Reader) (n int64, err error)
}

注意:ReadFrom 方法不會(huì)返回 err == EOF默赂。

下面的例子簡(jiǎn)單的實(shí)現(xiàn)將文件中的數(shù)據(jù)全部讀取(顯示在標(biāo)準(zhǔn)輸出):

file, err := os.Open("./write_at.txt")
    if err != nil {
        fmt.Println(err.Error())
        return
    }

    defer func() {
        err = file.Close()
        if err != nil {
            fmt.Println(err.Error())
        }
    }()
    writer := bufio.NewWriter(os.Stdin)
    n, err := writer.ReadFrom(file)
    if err != nil {
        fmt.Println(err.Error(), n)
        return
    }
    err = writer.Flush()
    if err != nil {
        fmt.Println(err.Error())
        return
    }
//輸出:一起學(xué)習(xí)Go語言,用我的方式%

?當(dāng)然括勺,我們可以通過 ioutil 包的 ReadFile 函數(shù)獲取文件全部?jī)?nèi)容缆八。其實(shí)曲掰,跟蹤一下 ioutil.ReadFile 的源碼,會(huì)發(fā)現(xiàn)其實(shí)也是通過 ReadFrom 方法實(shí)現(xiàn)(用的是 bytes.Buffer奈辰,它實(shí)現(xiàn)了 ReaderFrom 接口)栏妖。

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

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

這里不給出實(shí)現(xiàn)代碼了翰守,有興趣的可以實(shí)現(xiàn)一下孵奶。
提示
通過查看 bufio.Writer 或 strings.Buffer 類型的 ReadFrom 方法實(shí)現(xiàn),會(huì)發(fā)現(xiàn)蜡峰,其實(shí)它們的實(shí)現(xiàn)和上面說的第 2 種思路類似了袁。
WriterTo的定義如下:

/*
WriteTo 將數(shù)據(jù)寫入 w 中,直到?jīng)]有數(shù)據(jù)可寫或發(fā)生錯(cuò)誤湿颅。其返回值 n 為寫入的字節(jié)數(shù)载绿。 在寫入過程中遇到的任何錯(cuò)誤也將被返回。
如果 WriterTo 可用油航,Copy 函數(shù)就會(huì)使用它崭庸。
*/
type WriterTo interface {
    WriteTo(w Writer) (n int64, err error)
}

讀者是否發(fā)現(xiàn),其實(shí) ReaderFrom 和 WriterTo 接口的方法接收的參數(shù)是 io.Reader 和 io.Writer 類型谊囚。根據(jù) io.Reader 和 io.Writer 接口的講解怕享,對(duì)該接口的使用應(yīng)該可以很好的掌握。

這里只提供簡(jiǎn)單的一個(gè)示例代碼:將一段文本輸出到標(biāo)準(zhǔn)輸出

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

通過 io.ReaderFrom 和 io.WriterTo 的學(xué)習(xí)镰踏,我們知道函筋,如果這樣的需求,可以考慮使用這兩個(gè)接口:“一次性從某個(gè)地方讀或?qū)懙侥硞€(gè)地方去奠伪〉剩”

Seeker 接口

接口定義如下:

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

官方文檔中關(guān)于該接口方法的說明:

Seek 設(shè)置下一次 Read 或 Write 的偏移量為 offset,它的解釋取決于 whence: 0 表示相對(duì)于文件的起始處绊率,1 表示相對(duì)于當(dāng)前的偏移谨敛,而 2 表示相對(duì)于其結(jié)尾處。 Seek 返回新的偏移量和一個(gè)錯(cuò)誤滤否,如果有的話脸狸。

?也就是說,Seek 方法是用于設(shè)置偏移量的藐俺,這樣可以從某個(gè)特定位置開始操作數(shù)據(jù)流肥惭。聽起來和 ReaderAt/WriteAt 接口有些類似盯仪,不過 Seeker 接口更靈活紊搪,可以更好的控制讀寫數(shù)據(jù)流的位置蜜葱。

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

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

小貼士
whence 的值耀石,在 io 包中定義了相應(yīng)的常量牵囤,應(yīng)該使用這些常量

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 包中的常量已經(jīng)被標(biāo)注為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
)

Closer接口

接口定義如下:

type Closer interface {
    Close() error
}

該接口比較簡(jiǎn)單,只有一個(gè) Close() 方法滞伟,用于關(guān)閉數(shù)據(jù)流揭鳞。
文件 (os.File)、歸檔(壓縮包)梆奈、數(shù)據(jù)庫連接野崇、Socket 等需要手動(dòng)關(guān)閉的資源都實(shí)現(xiàn)了 Closer 接口。
實(shí)際編程中亩钟,經(jīng)常將 Close 方法的調(diào)用放在 defer 語句中乓梨。
小提示
初學(xué)者容易寫出這樣的代碼:

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

?當(dāng)文件 studygolang.txt 不存在或找不到時(shí),file.Close() 會(huì) panic清酥,因?yàn)?file 是 nil扶镀。因此,應(yīng)該將 defer file.Close() 放在錯(cuò)誤檢查之后焰轻。
經(jīng)過 issue40 提醒臭觉,查看源碼:

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

可見并不會(huì) panic,但在 Close 之前校驗(yàn)錯(cuò)誤是個(gè)好習(xí)慣辱志!

其他接口

ByteReader 和 ByteWriter

通過名稱大概也能猜出這組接口的用途:讀或?qū)懸粋€(gè)字節(jié)蝠筑。接口定義如下:

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

type ByteWriter interface {
    WriteByte(c byte) error
}

在標(biāo)準(zhǔn)庫中,有如下類型實(shí)現(xiàn)了 io.ByteReader 或 io.ByteWriter:
-bufio.Reader/Writer 分別實(shí)現(xiàn)了io.ByteReader 和 io.ByteWriter
-bytes.Buffer 同時(shí)實(shí)現(xiàn)了 io.ByteReader 和 io.ByteWriter
-bytes.Reader 實(shí)現(xiàn)了 io.ByteReader
-strings.Reader 實(shí)現(xiàn)了 io.ByteReader

接下來的示例中揩懒,我們通過 bytes.Buffer 來一次讀取或?qū)懭胍粋€(gè)字節(jié)(主要代碼):

var ch byte
fmt.Scanf("%c\n", &ch)

buffer := new(bytes.Buffer)
err := buffer.WriteByte(ch)
if err == nil {
    fmt.Println("寫入一個(gè)字節(jié)成功什乙!準(zhǔn)備讀取該字節(jié)……")
    newCh, _ := buffer.ReadByte()
    fmt.Printf("讀取的字節(jié):%c\n", newCh)
} else {
    fmt.Println("寫入錯(cuò)誤")
}

?程序從標(biāo)準(zhǔn)輸入接收一個(gè)字節(jié)(ASCII 字符),調(diào)用 buffer 的 WriteByte 將該字節(jié)寫入 buffer 中旭从,之后通過 ReadByte 讀取該字節(jié)稳强。
?一般地,我們不會(huì)使用 bytes.Buffer 來一次讀取或?qū)懭胍粋€(gè)字節(jié)和悦。那么退疫,這兩個(gè)接口有哪些用處呢?
?在標(biāo)準(zhǔn)庫 encoding/binary 中鸽素,實(shí)現(xiàn)Google-ProtoBuf中的 Varints 讀取褒繁,ReadVarint 就需要一個(gè) io.ByteReader 類型的參數(shù),也就是說馍忽,它需要一個(gè)字節(jié)一個(gè)字節(jié)的讀取棒坏。關(guān)于 encoding/binary 包在后面會(huì)詳細(xì)介紹燕差。
?在標(biāo)準(zhǔn)庫 image/jpeg 中,Encode函數(shù)的內(nèi)部實(shí)現(xiàn)使用了 ByteWriter 寫入一個(gè)字節(jié)坝冕。
小貼士
?可以通過在 Go 語言源碼 src/pkg 中搜索 "io.ByteReader" 或 "io.ByteWiter"徒探,獲得哪些地方用到了這兩個(gè)接口。你會(huì)發(fā)現(xiàn)喂窟,這兩個(gè)接口在二進(jìn)制數(shù)據(jù)或歸檔壓縮時(shí)用的比較多测暗。

ByteScanner、RuneReader 和 RuneScanner

將這三個(gè)接口放在一起磨澡,是考慮到與 ByteReader 相關(guān)或相應(yīng)碗啄。
ByteScanner 接口的定義如下:

type ByteScanner interface {
    ByteReader
    UnreadByte() error
}

?可見,它內(nèi)嵌了 ByteReader 接口(可以理解為繼承了 ByteReader 接口)稳摄,UnreadByte 方法的意思是:將上一次 ReadByte 的字節(jié)還原稚字,使得再次調(diào)用 ReadByte 返回的結(jié)果和上一次調(diào)用相同,也就是說厦酬,UnreadByte 是重置上一次的 ReadByte胆描。注意,UnreadByte 調(diào)用之前必須調(diào)用了 ReadByte弃锐,且不能連續(xù)調(diào)用 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,錯(cuò)誤為:bytes.Buffer: UnreadByte: previous operation was not a read
?RuneReader 接口和 ByteReader 類似霹菊,只是 ReadRune 方法讀取單個(gè) UTF-8 字符剧蚣,返回其 rune 和該字符占用的字節(jié)數(shù)。該接口在 regexp 包有用到旋廷。
問題:strings.Index("行業(yè)交流群", "交流") 返回的是單字節(jié)字符的位置:6鸠按。但是想要的是 unicode 字符的位置:2
這里借助utf8的RuneCountInString函數(shù),實(shí)現(xiàn)代碼如下:

// strings.Index 的 UTF-8 版本
// 即 Utf8Index("Go語言中文網(wǎng)", "中文") 返回 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 類似饶碘,就不贅述了目尖。

ReadCloser、ReadSeeker扎运、ReadWriteCloser瑟曲、ReadWriteSeeker、ReadWriter豪治、WriteCloser 和 WriteSeeker 接口

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

type ReadWriter interface {
    Reader
    Writer
}

?這是 Reader 接口和 Writer 接口的簡(jiǎn)單組合(內(nèi)嵌)。
?這些接口的作用是:有些時(shí)候同時(shí)需要某兩個(gè)接口的所有功能负拟,即必須同時(shí)實(shí)現(xiàn)了某兩個(gè)接口的類型才能夠被傳入使用烦衣。可見,io 包中有大量的“小接口”花吟,這樣方便組合為“大接口”秸歧。

SectionReader 類型

SectionReader 是一個(gè) struct(沒有任何導(dǎo)出的字段),實(shí)現(xiàn)了 Read, Seek 和 ReadAt衅澈,同時(shí)键菱,內(nèi)嵌了 ReaderAt 接口。結(jié)構(gòu)定義如下

type SectionReader struct {
    r     ReaderAt    // 該類型最終的 Read/ReadAt 最終都是通過 r 的 ReadAt 實(shí)現(xiàn)
    base  int64        // NewSectionReader 會(huì)將 base 設(shè)置為 off
    off   int64        // 從 r 中的 off 偏移處開始讀取數(shù)據(jù)
    limit int64        // limit - off = SectionReader 流的長(zhǎng)度
}

從名稱我們可以猜到矾麻,該類型讀取數(shù)據(jù)流中部分?jǐn)?shù)據(jù)纱耻。看一下

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

的文檔說明就知道了:

?NewSectionReader 返回一個(gè) SectionReader险耀,它從 r 中的偏移量 off 處讀取 n 個(gè)字節(jié)后以 EOF 停止。

?也就是說玖喘,SectionReader 只是內(nèi)部(內(nèi)嵌)ReaderAt 表示的數(shù)據(jù)流的一部分:從 off 開始后的 n 個(gè)字節(jié)甩牺。
?這個(gè)類型的作用是:方便重復(fù)操作某一段 (section) 數(shù)據(jù)流;或者同時(shí)需要 ReadAt 和 Seek 的功能累奈。
?由于該類型所支持的操作贬派,前面都有介紹,因此不提供示例代碼了澎媒。
LimitedReader 類型
LimitedReader 結(jié)構(gòu)定義如下:

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

文檔說明如下:

從 R 讀取但將返回的數(shù)據(jù)量限制為 N 字節(jié)。每調(diào)用一次 Read 都將更新 N 來反應(yīng)新的剩余數(shù)量戒努。

也就是說请敦,最多只能返回 N 字節(jié)數(shù)據(jù)。
LimitedReader 只實(shí)現(xiàn)了 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

可見侍筛,通過該類型可以達(dá)到 只允許讀取一定長(zhǎng)度數(shù)據(jù) 的目的。
在 io 包中撒穷,LimitReader 函數(shù)的實(shí)現(xiàn)其實(shí)就是調(diào)用 LimitedReader:

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

PipeReader 和 PipeWriter 類型

PipeReader(一個(gè)沒有任何導(dǎo)出字段的 struct)是管道的讀取端匣椰。它實(shí)現(xiàn)了 io.Reader 和 io.Closer 接口。結(jié)構(gòu)定義如下:

type PipeReader struct {
    p *pipe
}

?關(guān)于 PipeReader.Read 方法的說明:從管道中讀取數(shù)據(jù)端礼。該方法會(huì)堵塞禽笑,直到管道寫入端開始寫入數(shù)據(jù)或?qū)懭攵吮魂P(guān)閉。如果寫入端關(guān)閉時(shí)帶有 error(即調(diào)用 CloseWithError 關(guān)閉)蛤奥,該Read返回的 err 就是寫入端傳遞的error佳镜;否則 err 為 EOF。
?PipeWriter(一個(gè)沒有任何導(dǎo)出字段的 struct)是管道的寫入端喻括。它實(shí)現(xiàn)了 io.Writer 和 io.Closer 接口邀杏。結(jié)構(gòu)定義如下:

type PipeWriter struct {
    p *pipe
}

?關(guān)于 PipeWriter.Write 方法的說明:寫數(shù)據(jù)到管道中。該方法會(huì)堵塞,直到管道讀取端讀完所有數(shù)據(jù)或讀取端被關(guān)閉望蜡。如果讀取端關(guān)閉時(shí)帶有 error(即調(diào)用 CloseWithError 關(guān)閉)唤崭,該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語言中文網(wǎng)")
    for i := 0; i < 3; i++{
        n, err := writer.Write(data)
        if err != nil{
            fmt.Println(err)
            return
        }
        fmt.Printf("寫入字節(jié) %d\n",n)
    }
    writer.CloseWithError(errors.New("寫入段已關(guān)閉"))
}

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("收到字節(jié): %d\n buf內(nèi)容: %s\n",n,buf)
    }
}

io.Pipe() 用于創(chuàng)建一個(gè)同步的內(nèi)存管道 (synchronous in-memory pipe)谢肾,函數(shù)簽名:

func Pipe() (*PipeReader, *PipeWriter)

?它將 io.Reader 連接到 io.Writer。一端的讀取匹配另一端的寫入小泉,直接在這兩端之間復(fù)制數(shù)據(jù)芦疏;它沒有內(nèi)部緩存。它對(duì)于并行調(diào)用 Read 和 Write 以及其它函數(shù)或 Close 來說都是安全的微姊。一旦等待的 I/O 結(jié)束酸茴,Close 就會(huì)完成。并行調(diào)用 Read 或并行調(diào)用 Write 也同樣安全:同種類的調(diào)用將按順序進(jìn)行控制兢交。
?正因?yàn)槭峭降男胶矗虼瞬荒茉谝粋€(gè) goroutine 中進(jìn)行讀和寫。
?另外配喳,對(duì)于管道的 close 方法(非 CloseWithError 時(shí))酪穿,err 會(huì)被置為 EOF。

Copy 和 CopyN 函數(shù)

Copy 函數(shù)的簽名:

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

函數(shù)文檔:

?Copy 將 src 復(fù)制到 dst晴裹,直到在 src 上到達(dá) EOF 或發(fā)生錯(cuò)誤被济。它返回復(fù)制的字節(jié)數(shù),如果有錯(cuò)誤的話涧团,還會(huì)返回在復(fù)制時(shí)遇到的第一個(gè)錯(cuò)誤只磷。
?成功的 Copy 返回 err == nil,而非 err == EOF少欺。由于 Copy 被定義為從 src 讀取直到 EOF 為止喳瓣,因此它不會(huì)將來自 Read 的 EOF 當(dāng)做錯(cuò)誤來報(bào)告。
?若 dst 實(shí)現(xiàn)了 ReaderFrom 接口赞别,其復(fù)制操作可通過調(diào)用 dst.ReadFrom(src) 實(shí)現(xiàn)畏陕。此外,若 src 實(shí)現(xiàn)了 WriterTo 接口仿滔,其復(fù)制操作可通過調(diào)用 src.WriteTo(dst) 實(shí)現(xiàn)惠毁。

代碼:

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

直接將內(nèi)容輸出(寫入 Stdout 中)。
我們甚至可以這么做:

package main

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

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

執(zhí)行:echo "Hello, World" | go run main.go
CopyN 函數(shù)的簽名:

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

函數(shù)文檔:

CopyN 將 n 個(gè)字節(jié)(或到一個(gè)error)從 src 復(fù)制到 dst崎页。 它返回復(fù)制的字節(jié)數(shù)以及在復(fù)制時(shí)遇到的最早的錯(cuò)誤鞠绰。當(dāng)且僅當(dāng)err == nil時(shí),written == n 。
若 dst 實(shí)現(xiàn)了 ReaderFrom 接口飒焦,復(fù)制操作也就會(huì)使用它來實(shí)現(xiàn)蜈膨。

代碼:

io.CopyN(os.Stdout, strings.NewReader("Go語言中文網(wǎng)"), 8)
//會(huì)輸出:Go語言

ReadAtLeast 和 ReadFull 函數(shù)

ReadAtLeast 函數(shù)的簽名:

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

函數(shù)文檔:

ReadAtLeast 將 r 讀取到 buf 中屿笼,直到讀了最少 min 個(gè)字節(jié)為止。它返回復(fù)制的字節(jié)數(shù)翁巍,如果讀取的字節(jié)較少驴一,還會(huì)返回一個(gè)錯(cuò)誤。若沒有讀取到字節(jié)灶壶,錯(cuò)誤就只是 EOF肝断。如果一個(gè) EOF 發(fā)生在讀取了少于 min 個(gè)字節(jié)之后,ReadAtLeast 就會(huì)返回 ErrUnexpectedEOF驰凛。若 min 大于 buf 的長(zhǎng)度胸懈,ReadAtLeast 就會(huì)返回 ErrShortBuffer。對(duì)于返回值恰响,當(dāng)且僅當(dāng) err == nil 時(shí)趣钱,才有 n >= min。

一般可能不太會(huì)用到這個(gè)函數(shù)渔隶。使用時(shí)需要注意返回的 error 判斷羔挡。
ReadFull 函數(shù)的簽名:

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

函數(shù)文檔:

ReadFull 精確地從 r 中將 len(buf) 個(gè)字節(jié)讀取到 buf 中。它返回復(fù)制的字節(jié)數(shù)间唉,如果讀取的字節(jié)較少,還會(huì)返回一個(gè)錯(cuò)誤利术。若沒有讀取到字節(jié)呈野,錯(cuò)誤就只是 EOF。如果一個(gè) EOF 發(fā)生在讀取了一些但不是所有的字節(jié)后印叁,ReadFull 就會(huì)返回 ErrUnexpectedEOF被冒。對(duì)于返回值,當(dāng)且僅當(dāng) err == nil 時(shí)轮蜕,才有 n == len(buf)昨悼。

注意該函數(shù)和 ReadAtLeast 的區(qū)別:ReadFull 將 buf 讀滿;而 ReadAtLeast 是最少讀取 min 個(gè)字節(jié)跃洛。

WriteString 函數(shù)

這是為了方便寫入 string 類型提供的函數(shù)率触,函數(shù)簽名:

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

函數(shù)文檔:

WriteString 將s的內(nèi)容寫入w中,當(dāng) w 實(shí)現(xiàn)了 WriteString 方法時(shí)汇竭,會(huì)直接調(diào)用該方法葱蝗,否則執(zhí)行 w.Write([]byte(s))。

MultiReader 和 MultiWriter 函數(shù)

這兩個(gè)函數(shù)的定義分別是:

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

它們接收多個(gè) Reader 或 Writer细燎,返回一個(gè) Reader 或 Writer两曼。我們可以猜想到這兩個(gè)函數(shù)就是操作多個(gè) Reader 或 Writer 就像操作一個(gè)。

事實(shí)上玻驻,在 io 包中定義了兩個(gè)非導(dǎo)出類型:mutilReader 和 multiWriter悼凑,它們分別實(shí)現(xiàn)了 io.Reader 和 io.Writer 接口。類型定義為:

type multiReader struct {
    readers []Reader
}

type multiWriter struct {
    writers []Writer
}

對(duì)于這兩種類型對(duì)應(yīng)的實(shí)現(xiàn)方法(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

代碼中首先構(gòu)造了一個(gè) io.Reader 的 slice渐夸,由 strings.Reader 和 bytes.Buffer 兩個(gè)實(shí)例組成,然后通過 MultiReader 得到新的 Reader寸莫,循環(huán)讀取新 Reader 中的內(nèi)容捺萌。從輸出結(jié)果可以看到,第一次調(diào)用 Reader 的 Read 方法獲取到的是 slice 中第一個(gè)元素的內(nèi)容……也就是說膘茎,MultiReader 只是邏輯上將多個(gè) Reader 組合起來桃纯,并不能通過調(diào)用一次 Read 方法獲取所有 Reader 的內(nèi)容。在所有的 Reader 內(nèi)容都被讀完后披坏,Reader 會(huì)返回 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語言中文網(wǎng)"))

這段程序執(zhí)行后在生成 tmp.txt 文件,同時(shí)在文件和屏幕中都輸出:Go語言中文網(wǎng)棒拂。這和 Unix 中的 tee 命令類似伞梯。

動(dòng)手試試

Go 實(shí)現(xiàn) Unix 中 tee 命令的功能很簡(jiǎn)單吧。MultiWriter 的 Write 方法是如何實(shí)現(xiàn)的帚屉?有興趣可以自己實(shí)現(xiàn)一個(gè)谜诫,然后對(duì)著源碼比較一下。

TeeReader函數(shù)

函數(shù)簽名如下:

func TeeReader(r Reader, w Writer) Reader

TeeReader 返回一個(gè) Reader攻旦,它將從 r 中讀到的數(shù)據(jù)寫入 w 中漾唉。所有經(jīng)由它處理的從 r 的讀取都匹配于對(duì)應(yīng)的對(duì) w 的寫入谦铃。它沒有內(nèi)部緩存婶博,即寫入必須在讀取完成前完成耙册。任何在寫入時(shí)遇到的錯(cuò)誤都將作為讀取錯(cuò)誤返回。

也就是說烙无,我們通過 Reader 讀取內(nèi)容后锋谐,會(huì)自動(dòng)寫入到 Writer 中去。例子代碼如下:

reader := io.TeeReader(strings.NewReader("Go語言中文網(wǎng)"), os.Stdout)
reader.Read(make([]byte, 20))
//輸出結(jié)果:Go語言中文網(wǎng)

這種功能的實(shí)現(xiàn)其實(shí)挺簡(jiǎn)單截酷,無非是在 Read 完后執(zhí)行 Write涮拗。

至此,io 所有接口合搅、類型和函數(shù)都講解完成多搀。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市灾部,隨后出現(xiàn)的幾起案子康铭,更是在濱河造成了極大的恐慌,老刑警劉巖赌髓,帶你破解...
    沈念sama閱讀 218,204評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件从藤,死亡現(xiàn)場(chǎng)離奇詭異催跪,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)夷野,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,091評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門懊蒸,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人悯搔,你說我怎么就攤上這事骑丸。” “怎么了妒貌?”我有些...
    開封第一講書人閱讀 164,548評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵通危,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我灌曙,道長(zhǎng)菊碟,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,657評(píng)論 1 293
  • 正文 為了忘掉前任在刺,我火速辦了婚禮逆害,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘蚣驼。我一直安慰自己魄幕,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,689評(píng)論 6 392
  • 文/花漫 我一把揭開白布颖杏。 她就那樣靜靜地躺著梅垄,像睡著了一般。 火紅的嫁衣襯著肌膚如雪输玷。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,554評(píng)論 1 305
  • 那天靡馁,我揣著相機(jī)與錄音欲鹏,去河邊找鬼。 笑死臭墨,一個(gè)胖子當(dāng)著我的面吹牛赔嚎,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播胧弛,決...
    沈念sama閱讀 40,302評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼尤误,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了结缚?” 一聲冷哼從身側(cè)響起损晤,我...
    開封第一講書人閱讀 39,216評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎红竭,沒想到半個(gè)月后尤勋,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體喘落,經(jīng)...
    沈念sama閱讀 45,661評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,851評(píng)論 3 336
  • 正文 我和宋清朗相戀三年最冰,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了瘦棋。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,977評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡暖哨,死狀恐怖赌朋,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情篇裁,我是刑警寧澤沛慢,帶...
    沈念sama閱讀 35,697評(píng)論 5 347
  • 正文 年R本政府宣布,位于F島的核電站茴恰,受9級(jí)特大地震影響颠焦,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜往枣,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,306評(píng)論 3 330
  • 文/蒙蒙 一伐庭、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧分冈,春花似錦圾另、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,898評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至坡椒,卻和暖如春扰路,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背倔叼。 一陣腳步聲響...
    開封第一講書人閱讀 33,019評(píng)論 1 270
  • 我被黑心中介騙來泰國(guó)打工汗唱, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人丈攒。 一個(gè)月前我還...
    沈念sama閱讀 48,138評(píng)論 3 370
  • 正文 我出身青樓哩罪,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親巡验。 傳聞我的和親對(duì)象是個(gè)殘疾皇子际插,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,927評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容