今天,我們來講另一個與 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。顯而易見,在壓縮之後,再讀取位元組就肯定要從緩沖區的頭部開始讀了。

(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方法,顯然是最穩妥的。
今天我們從“bufio.Reader類型值中的緩沖區起着怎樣的作用”這道問題入手,介紹了一部分 bufio 包中的資料類型,在下一次的分享中,我會沿着這個問題繼續展開。
https://github.com/MingsonZheng/go-core-demo
本作品采用知識共享署名-非商業性使用-相同方式共享 4.0 國際許可協定進行許可。
歡迎轉載、使用、重新釋出,但務必保留文章署名 鄭子銘 (包含連結: http://www.cnblogs.com/MingsonZheng/ ),不得用于商業目的,基于本文修改後的作品務必以相同的許可釋出。