Go語言核心36講(Go語言實戰與應用二十)--學習筆記


42 | bufio包中的數據類型 (上)

今天,我們來講另一個與 I/O 操作強相關的代碼包bufio。bufio是“buffered I/O”的縮寫。顧名思義,這個代碼包中的程序實體實現的 I/O 操作都內置了緩沖區。

bufio包中的數據類型主要有:

1、Reader;

2、Scanner;

3、Writer和ReadWriter。

與io包中的數據類型類似,這些類型的值也都需要在初始化的時候,包裝一個或多個簡單 I/O 接口類型的值。(這里的簡單 I/O 接口類型指的就是io包中的那些簡單接口。)

下面,我們將通過一系列問題對bufio.Reader類型和bufio.Writer類型進行討論(以前者為主)。今天我的問題是:bufio.Reader類型值中的緩沖區起着怎樣的作用?

這道題的典型回答是這樣的。

bufio.Reader類型的值(以下簡稱Reader值)內的緩沖區,其實就是一個數據存儲中介,它介於底層讀取器與讀取方法及其調用方之間。所謂的底層讀取器,就是在初始化此類值的時候傳入的io.Reader類型的參數值。

Reader值的讀取方法一般都會先從其所屬值的緩沖區中讀取數據。同時,在必要的時候,它們還會預先從底層讀取器那里讀出一部分數據,並暫存於緩沖區之中以備后用。

有這樣一個緩沖區的好處是,可以在大多數的時候降低讀取方法的執行時間。雖然,讀取方法有時還要負責填充緩沖區,但從總體來看,讀取方法的平均執行時間一般都會因此有大幅度的縮短。

問題解析

bufio.Reader類型並不是開箱即用的,因為它包含了一些需要顯式初始化的字段。為了讓你能在后面更好地理解它的讀取方法的內部流程,我先在這里簡要地解釋一下這些字段,如下所示。

1、buf:[]byte類型的字段,即字節切片,代表緩沖區。雖然它是切片類型的,但是其長度卻會在初始化的時候指定,並在之后保持不變。

2、rd:io.Reader類型的字段,代表底層讀取器。緩沖區中的數據就是從這里拷貝來的。

3、r:int類型的字段,代表對緩沖區進行下一次讀取時的開始索引。我們可以稱它為已讀計數。

4、w:int類型的字段,代表對緩沖區進行下一次寫入時的開始索引。我們可以稱之為已寫計數。

5、err:error類型的字段。它的值用於表示在從底層讀取器獲得數據時發生的錯誤。這里的值在被讀取或忽略之后,該字段會被置為nil。

6、lastByte:int類型的字段,用於記錄緩沖區中最后一個被讀取的字節。讀回退時會用到它的值。

7、lastRuneSize:int類型的字段,用於記錄緩沖區中最后一個被讀取的 Unicode 字符所占用的字節數。讀回退的時候會用到它的值。這個字段只會在其所屬值的ReadRune方法中才會被賦予有意義的值。在其他情況下,它都會被置為-1。

bufio包為我們提供了兩個用於初始化Reader值的函數,分別叫:

  • NewReader;
  • NewReaderSize;

它們都會返回一個*bufio.Reader類型的值。

NewReader函數初始化的Reader值會擁有一個默認尺寸的緩沖區。這個默認尺寸是 4096 個字節,即:4 KB。而NewReaderSize函數則將緩沖區尺寸的決定權拋給了使用方。

由於這里的緩沖區在一個Reader值的生命周期內其尺寸不可變,所以在有些時候是需要做一些權衡的。NewReaderSize函數就提供了這樣一個途徑。

在bufio.Reader類型擁有的讀取方法中,Peek方法和ReadSlice方法都會調用該類型一個名為fill的包級私有方法。fill方法的作用是填充內部緩沖區。我們在這里就先重點說說它。

fill方法會先檢查其所屬值的已讀計數。如果這個計數不大於0,那么有兩種可能。

一種可能是其緩沖區中的字節都是全新的,也就是說它們都沒有被讀取過,另一種可能是緩沖區剛被壓縮過。

對緩沖區的壓縮包括兩個步驟。第一步,把緩沖區中在[已讀計數, 已寫計數)范圍之內的所有元素值(或者說字節)都依次拷貝到緩沖區的頭部。

比如,把緩沖區中與已讀計數代表的索引對應字節拷貝到索引0的位置,並把緊挨在它后邊的字節拷貝到索引1的位置,以此類推。

這一步之所以不會有任何副作用,是因為它基於兩個事實。

第一事實,已讀計數之前的字節都已經被讀取過,並且肯定不會再被讀取了,因此把它們覆蓋掉是安全的。

第二個事實,在壓縮緩沖區之后,已寫計數之后的字節只可能是已被讀取過的字節,或者是已被拷貝到緩沖區頭部的未讀字節,又或者是代表未曾被填入數據的零值0x00。所以,后續的新字節是可以被寫到這些位置上的。

在壓縮緩沖區的第二步中,fill方法會把已寫計數的新值設定為原已寫計數與原已讀計數的差。這個差所代表的索引,就是壓縮后第一次寫入字節時的開始索引。

另外,該方法還會把已讀計數的值置為0。顯而易見,在壓縮之后,再讀取字節就肯定要從緩沖區的頭部開始讀了。

image

(bufio.Reader 中的緩沖區壓縮)

實際上,fill方法只要在開始時發現其所屬值的已讀計數大於0,就會對緩沖區進行一次壓縮。之后,如果緩沖區中還有可寫的位置,那么該方法就會對其進行填充。

在填充緩沖區的時候,fill方法會試圖從底層讀取器那里,讀取足夠多的字節,並盡量把從已寫計數代表的索引位置到緩沖區末尾之間的空間都填滿。

在這個過程中,fill方法會及時地更新已寫計數,以保證填充的正確性和順序性。另外,它還會判斷從底層讀取器讀取數據的時候,是否有錯誤發生。如果有,那么它就會把錯誤值賦給其所屬值的err字段,並終止填充流程。

好了,到這里,我們暫告一個段落。在本題中,我對bufio.Reader類型的基本結構,以及相關的一些函數和方法進行了概括介紹,並且重點闡述了該類型的fill方法。

后者是我們在后面要說明的一些讀取流程的重要組成部分。你起碼要記住的是:這個fill方法大致都做了些什么。

知識擴展

問題 1:bufio.Writer類型值中緩沖的數據什么時候會被寫到它的底層寫入器?

我們先來看一下bufio.Writer類型都有哪些字段:

1、err:error類型的字段。它的值用於表示在向底層寫入器寫數據時發生的錯誤。

2、buf:[]byte類型的字段,代表緩沖區。在初始化之后,它的長度會保持不變。

3、n:int類型的字段,代表對緩沖區進行下一次寫入時的開始索引。我們可以稱之為已寫計數。

4、wr:io.Writer類型的字段,代表底層寫入器。

bufio.Writer類型有一個名為Flush的方法,它的主要功能是把相應緩沖區中暫存的所有數據,都寫到底層寫入器中。數據一旦被寫進底層寫入器,該方法就會把它們從緩沖區中刪除掉。

不過,這里的刪除有時候只是邏輯上的刪除而已。不論是否成功地寫入了所有的暫存數據,Flush方法都會妥當處置,並保證不會出現重寫和漏寫的情況。該類型的字段n在此會起到很重要的作用。

bufio.Writer類型值(以下簡稱Writer值)擁有的所有數據寫入方法都會在必要的時候調用它的Flush方法。

比如,Write方法有時候會在把數據寫進緩沖區之后,調用Flush方法,以便為后續的新數據騰出空間。WriteString方法的行為與之類似。

又比如,WriteByte方法和WriteRune方法,都會在發現緩沖區中的可寫空間不足以容納新的字節,或 Unicode 字符的時候,調用Flush方法。

此外,如果Write方法發現需要寫入的字節太多,同時緩沖區已空,那么它就會跨過緩沖區,並直接把這些數據寫到底層寫入器中。

而ReadFrom方法,則會在發現底層寫入器的類型是io.ReaderFrom接口的實現之后,直接調用其ReadFrom方法把參數值持有的數據寫進去。

總之,在通常情況下,只要緩沖區中的可寫空間無法容納需要寫入的新數據,Flush方法就一定會被調用。並且,bufio.Writer類型的一些方法有時候還會試圖走捷徑,跨過緩沖區而直接對接數據供需的雙方。

你可以在理解了這些內部機制之后,有的放矢地編寫你的代碼。不過,在你把所有的數據都寫入Writer值之后,再調用一下它的Flush方法,顯然是最穩妥的。

package main

import (
	"bufio"
	"fmt"
	"strings"
)

func main() {
	comment := "Package bufio implements buffered I/O. " +
		"It wraps an io.Reader or io.Writer object, " +
		"creating another object (Reader or Writer) that " +
		"also implements the interface but provides buffering and " +
		"some help for textual I/O."
	basicReader := strings.NewReader(comment)
	fmt.Printf("The size of basic reader: %d\n", basicReader.Size())
	fmt.Println()

	// 示例1。
	fmt.Println("New a buffered reader ...")
	reader1 := bufio.NewReader(basicReader)
	fmt.Printf("The default size of buffered reader: %d\n", reader1.Size())
	// 此時reader1的緩沖區還沒有被填充。
	fmt.Printf("The number of unread bytes in the buffer: %d\n", reader1.Buffered())
	fmt.Println()

	// 示例2。
	bytes, err := reader1.Peek(7)
	if err != nil {
		fmt.Printf("error: %v\n", err)
	}
	fmt.Printf("Peeked contents(%d): %q\n", len(bytes), bytes)
	fmt.Printf("The number of unread bytes in the buffer: %d\n", reader1.Buffered())
	fmt.Println()

	// 示例3。
	buf1 := make([]byte, 7)
	n, err := reader1.Read(buf1)
	if err != nil {
		fmt.Printf("error: %v\n", err)
	}
	fmt.Printf("Read contents(%d): %q\n", n, buf1)
	fmt.Printf("The number of unread bytes in the buffer: %d\n", reader1.Buffered())
	fmt.Println()

	// 示例4。
	fmt.Printf("Reset the basic reader (size: %d) ...\n", len(comment))
	basicReader.Reset(comment)
	fmt.Printf("Reset the buffered reader (size: %d) ...\n", reader1.Size())
	reader1.Reset(basicReader)
	peekNum := len(comment) + 1
	fmt.Printf("Peek %d bytes ...\n", peekNum)
	bytes, err = reader1.Peek(peekNum)
	if err != nil {
		fmt.Printf("error: %v\n", err)
	}
	fmt.Printf("The number of peeked bytes: %d\n", len(bytes))
	fmt.Println()

	// 示例5。
	fmt.Printf("Reset the basic reader (size: %d) ...\n", len(comment))
	basicReader.Reset(comment)
	size := 300
	fmt.Printf("New a buffered reader with size %d ...\n", size)
	reader2 := bufio.NewReaderSize(basicReader, size)
	peekNum = size + 1
	fmt.Printf("Peek %d bytes ...\n", peekNum)
	bytes, err = reader2.Peek(peekNum)
	if err != nil {
		fmt.Printf("error: %v\n", err)
	}
	fmt.Printf("The number of peeked bytes: %d\n", len(bytes))
}

總結

今天我們從“bufio.Reader類型值中的緩沖區起着怎樣的作用”這道問題入手,介紹了一部分 bufio 包中的數據類型,在下一次的分享中,我會沿着這個問題繼續展開。

筆記源碼

https://github.com/MingsonZheng/go-core-demo

知識共享許可協議

本作品采用知識共享署名-非商業性使用-相同方式共享 4.0 國際許可協議進行許可。

歡迎轉載、使用、重新發布,但務必保留文章署名 鄭子銘 (包含鏈接: http://www.cnblogs.com/MingsonZheng/ ),不得用於商業目的,基於本文修改后的作品務必以相同的許可發布。


免責聲明!

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



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