Go36-42,43-bufio包
bufio包
這是另一個與I/O操作強相關的程式碼包。bufio是“buffered I/O”的縮寫,這個程式碼包中的程式實體實現的I/O操作都內建了緩衝區。
主要資料型別
bufio包中的資料型別主要有:
- Reader
- Scanner
- Writer和ReadWriter
與io包中的資料型別類似,這些型別的值也都需要在初始化的時候,包裝一個或多個簡單I/O介面型別的值。這裡的簡單I/O介面型別指的就是io包中的那些簡單介面。
緩衝區的作用(bufio.Reader)
bufio.Reader型別的值內的緩衝區其實就是一個數據儲存中介,它介於底層讀取器與讀取方法及其呼叫方之間。所謂的底層讀取器,就是在初始化此類值的時候傳入的io.Reader型別的引數值。
Reader值的讀取方法一般會先從其所屬值的緩衝區中讀取資料。同時,在必要的時候,還會預先從底層讀取器那裡讀出一部分資料,並暫存於緩衝區之中以備後用。有這樣一個緩衝區的好處是,可以在大多數的時候降低讀取方法的執行時間。雖然讀取方法有時還要負責填充緩衝區,但從總體來看,讀取方法的平均執行時間一般都會因此有大幅度的縮短。
bufio.Reader結構體中的欄位
bufio.Reader型別並不是開箱即用的,因為它包含了一些許可顯示初始化的欄位。結構體的定義如下:
type Reader struct { buf[]byte rdio.Reader // reader provided by the client r, wint// buf read and write positions errerror lastByteint lastRuneSize int }
簡要的解釋一下結構體中的欄位:
- buf,位元組切片,代表緩衝區。雖然它是切片型別,但是其長度會在初始化的時候指定,並且之後保持不變。
- rd,代表底層讀取器。緩衝區中的資料就是從這裡拷貝出來的。
- r,代表對緩衝區進行下一次讀取時的開始索引。可以稱它為已讀計數 。
- w,代表對緩衝區進行下一次寫入是的開始縮寫。可以稱它為已寫計數 。
- err,它的值表示在從底層讀取器獲得資料時發生的錯誤。這裡的值在被讀取或忽略之後,該欄位會被置為nil。
- lastByte,記錄緩衝區最後一個被讀取的位元組。讀回退時會用到它的值。
- lastRuneSize,記錄緩衝區最後一個被讀取的Unicode字元所佔用的位元組數。讀回退的時候會用到它的值。這個欄位只會在其所屬值的ReadRune方法中才會被賦予有意義的值。其他它情況都會被置為-1。
初始化函式
bufio包提供了兩個用於用於初始化Reader值的函式,都會返回一個*bufio.Reader型別的值:
- NewReader
- NewReaderSze
NewReader函式初始化的Reader值會擁有一個預設尺寸的緩衝區。這個預設尺寸是4096個位元組,即:4KB:
const ( defaultBufSize = 4096 ) func NewReader(rd io.Reader) *Reader { return NewReaderSize(rd, defaultBufSize) } func NewReaderSize(rd io.Reader, size int) *Reader { // 內部程式碼省略 }
NewReaderSize函式則將緩衝區尺寸的決定權拋給了使用方。從上面的原始碼看,NewReader函式就是呼叫NewReaderSize的時候,指定了第二個用於決定緩衝區尺寸的引數。 初始化函式的示例:
func main() { comment := "TEST" basicReader := strings.NewReader(comment) fmt.Println(basicReader.Size()) reader1 := bufio.NewReader(basicReader) fmt.Println(reader1.Size()) reader2 := bufio.NewReaderSize(basicReader, 128) fmt.Println(reader2.Size()) }
由於這裡的緩衝區在一個Reader值的宣告週期內其尺寸不可變,所以在有些時候是需要做一些權衡的。NewReaderSize函式就提供了這樣一個途徑。
填充緩衝區(fill方法)
在bufio.Reader型別擁有的讀取方法中,Peek方法和ReadSlice方法都會呼叫該型別的一個名為fill的包級私有方法。fill方法的作用是填充內部緩衝區。
fill方法會先檢查其所屬值的已讀計數。如果這個計數不大於0,那麼有兩種可能:
- 緩衝區中的位元組都是全新的,就是它們都沒有被讀取過
- 緩衝區剛被壓縮過
壓縮緩衝區的壓縮包括兩個步驟:
- 把緩衝區中在[已讀計數, 已寫計數)範圍之內的所有元素值(或者說位元組)都依次拷貝到緩衝區的頭部
- 把已寫計數的新值設定為原已寫計數與原已讀計數的差。這個差代表的索引,就是壓縮後第一次寫入位元組時的開始索引
另外,fill方法還會把已讀計數的值置為0,顯然,在壓縮之後,再讀取位元組就是從緩衝區的頭部開始讀了。
實際上,fill方法只要在開始時發現其所屬值的已讀計數大於0,就會對緩衝區進行一次壓縮。之後,如果緩衝區還有可寫的位置,那麼該方法就會對其進行填充。
填充在填充緩衝區的時候,fill方法會試圖從底層讀取器那裡,讀取足夠多的位元組,並儘量把從已寫計數代表的索引位置到緩衝區末尾之間的空間都填滿。在這個過程中,fill方法會及時的更新已寫計數,以保證填充的正確性和順序性。另外,它還會判斷從底層讀取器讀取資料的時候,是否有錯誤發生。如果有,那麼它就會把錯誤值賦給其所屬值的err欄位,並終止填充流程。
示例程式碼下面是一個Peek方法使用的示例:
package main import ( "bufio" "fmt" "os" "strings" ) func main() { comment := "Hello, World!" basicReader := strings.NewReader(comment) fmt.Printf("字串長度: %d\n", basicReader.Size()) reader := bufio.NewReader(basicReader) fmt.Println("緩衝區長度:", reader.Size()) // 此時緩衝區還沒有被填充 fmt.Println("緩衝區裡的未讀位元組數:", reader.Buffered()) bytes, err := reader.Peek(5) if err != nil { fmt.Fprintf(os.Stderr, "ERROR: %v\n", err) } fmt.Printf("Peek讀取(%d): %q\n", len(bytes), bytes) fmt.Println("緩衝區裡的未讀位元組數:", reader.Buffered()) }
bufio.Writer
bufio.Writer型別有一個Flush方法,它的主要功能是把相應緩衝區中暫存的所有資料,都寫到底層寫入器中。資料一旦被寫進底層寫入器,該方法就會把這些資料從緩衝區中刪除掉。這裡的刪除有時候只是邏輯上的刪除而已。不論是否成功的寫入了所有的暫存資料,Flush方法都會妥當處置,並保證不會出現重寫和漏寫的情況。該型別的欄位n在此會起到很重要的作用。
bufio.Writer結構體中的欄位
bufio.Writer結構體的定義如下:
type Writer struct { err error buf []byte nint wrio.Writer }
欄位說明:
- err,用於表示在向底層寫上器寫資料時發生的錯誤。
- buf,代表緩衝區。在初始化之後,它的長度會保持不變。
- n,代表對緩衝區進行下一次寫入時的開始索引。可以稱之為已寫計數 。
- wr,代表底層寫入器。
Flush方法
bufio.Writer型別的值擁有的所有資料寫入方法都會在必要的時候呼叫Flush方法。
比如,Write方法有時候會在把資料寫進緩衝區之後,呼叫Flush方法,以便為後續的新資料騰出空間。WriteString方法的行為與之類似。
又比如,WriteByte方法和WriteRune方法,都會在發現緩衝區的可寫空間不足以容納新的位元組或Unicode字元的時候,呼叫Flush方法。
此外,如果Write方法發現需要寫入的位元組太多,同時緩衝區已空,那麼它就會跨過緩衝區,並直接把這些資料寫到底層寫入器中。
而ReadFrom,則會在發現底層寫入器的型別是io.ReaderFrom介面的實現之後,直接呼叫其ReadFrom方法把引數值持有的資料寫進去。
下面是一些示例程式碼:
package main import ( "bufio" "bytes" "fmt" "strings" ) func main() { comment := "Go is an open source programming language that makes it easy to build simple, " + "reliable," + "and efficient software." fmt.Println("全部的位元組數:", len(comment))// 112 basicWriter1 := &strings.Builder{} size := 64 writer1 := bufio.NewWriterSize(basicWriter1, size) fmt.Println("緩衝區大小:", size) fmt.Println() // WriteString方法呼叫Flush後,騰出空間 start, end := 0, 41 fmt.Println("寫入位元組數:", end-start) writer1.WriteString(comment[start:end]) fmt.Println("緩衝區使用位元組數:", writer1.Buffered()) fmt.Println("緩衝區可用位元組數:", writer1.Available()) fmt.Println("Flush方法重新整理緩衝區...") writer1.Flush() fmt.Println("緩衝區使用位元組數:", writer1.Buffered()) fmt.Println("緩衝區可用位元組數:", writer1.Available()) fmt.Println() // 寫入的位元組太多, start, end = 0, len(comment)// 全部讀完,所有的位元組數大於緩衝區的大小 fmt.Println("寫入位元組數:", end-start) writer1.WriteString(comment[start:end]) fmt.Println("緩衝區使用位元組數:", writer1.Buffered()) fmt.Println("緩衝區可用位元組數:", writer1.Available()) fmt.Println("Flush方法重新整理緩衝區...") writer1.Flush() fmt.Println() // ReadFrom會走捷徑,不使用緩衝區 basicWriter2 := &bytes.Buffer{} writer1.Reset(basicWriter2) reader := strings.NewReader(comment) writer1.ReadFrom(reader) fmt.Println("緩衝區使用位元組數:", writer1.Buffered()) fmt.Println("緩衝區可用位元組數:", writer1.Available()) }
總之,在通常情況下,只要緩衝區中的可寫空間無法容納需要寫入的新資料,Flush方法就一定會被呼叫。並且,bufio.Writer型別的一些方法有時候還會試圖走捷徑,跨過緩衝區而直接對接資料供需的雙方。可以在理解了這些內部機制之後,明確的在程式碼裡使用Flush方法。不過,也可以在把所有的資料都寫入Writer值之後,再呼叫一下它的Flush方法,這是最穩妥的做法。
讀取方法
bufio.Reader型別擁有很多用於讀取資料的指標方法,其中有4個方法可以作為不同讀取流程的代表:
- Peek
- Read
- ReadSlice
- ReadBytes
Peek方法
Peek方法的功能是:讀取並返回其緩衝區中的n個未讀位元組,並且它會從已讀計數代表的索引位置開始讀。Peek方法還有一個特點。就是即使它讀取了緩衝區中的資料,也不會更改已讀計數。
在緩衝區未被填滿,並且其中的未讀位元組的數量小於n的時候,該方法就會呼叫fill方法,以啟動緩衝區填充流程。但是,如果發現上次填充緩衝區的時候有錯誤,就不會再次填充了。
Peek方法的使用示例:
package main import ( "bufio" "fmt" "os" "strings" ) func main() { comment := "Go is an open source programming language that makes it easy to build simple, " + "reliable," + "and efficient software." basicReader := strings.NewReader(comment) fmt.Println("字串長度:", basicReader.Size()) size := 64 reader := bufio.NewReaderSize(basicReader, size) fmt.Println("緩衝區長度:", reader.Size()) // 此時緩衝區還沒有被填充 fmt.Println("緩衝區裡的未讀位元組數:", reader.Buffered()) fmt.Println() peekNum := 41 bytes, err := reader.Peek(peekNum) if err != nil { fmt.Fprintf(os.Stderr, "ERROR: %v\n", err) } fmt.Printf("Peek讀取(%d): %q\n", len(bytes), bytes) fmt.Println("緩衝區裡的未讀位元組數:", reader.Buffered()) fmt.Println() // Peek方法不改變已讀計數 // 把上面用Peek方法讀取的過程封裝一下,反覆呼叫 peek(reader, 2) peek(reader, 5) peek(reader, 8) } func peek(reader *bufio.Reader, n int) { bytes, err := reader.Peek(n) if err != nil { fmt.Fprintf(os.Stderr, "ERROR: %v\n", err) } fmt.Printf("Peek讀取(%d): %q\n", len(bytes), bytes) }
最開始,緩衝區為空,未讀位元組數量為0。呼叫Peek方法要讀取41個位元組。此時就會啟動緩衝區填充流程。緩衝區會被填滿,這裡緩衝區的大小設定為64,也就是填滿了64個位元組。然後讀取了41個位元組。由於Peek方法不會改變已讀計數,所以緩衝區裡的所有內容都是未讀的。所以,就算反覆呼叫Peek方法,讀到的內容也都是一樣的。
如果呼叫方法給定的n比緩衝區的長度還要大,或者緩衝區中未讀位元組的數量小於n,那麼Peek方法就會把所有未讀位元組返回,並且還會返回一個錯誤:
package main import ( "bufio" "fmt" "os" "strings" ) func main() { comment := "Hello, World!" basicReader := strings.NewReader(comment) // 緩衝區中未讀位元組數小於Peek方法指定的n reader1 := bufio.NewReader(basicReader) peekNum := len(comment) + 1 bytes, err := reader1.Peek(peekNum) if err != nil { fmt.Fprintf(os.Stderr, "ERROR: %v\n", err) } fmt.Printf("緩衝區中未讀位元組數: %d, Peek讀取: %d\n", reader1.Buffered(), peekNum) fmt.Printf("Peek讀取(%d): %q\n", len(bytes), bytes) fmt.Println() // Peek方法指定的n比緩衝區長度還要大 basicReader.Reset(comment) size := 300 reader2 := bufio.NewReaderSize(basicReader, size) peekNum = size + 1 bytes, err = reader2.Peek(peekNum) if err != nil { fmt.Fprintf(os.Stderr, "ERROR: %v\n", err) } fmt.Printf("緩衝區長度: %d, Peek讀取: %d\n", size, peekNum) fmt.Printf("Peek讀取(%d): %q\n", len(bytes), bytes) }
這裡兩種讀取錯誤的情況,都能正常返回讀取的內容。不過同時,還會返回一非nil的錯誤值。
Read方法
Read方法,在緩衝區中還有未讀位元組的情況下,它會把緩衝區中的未讀位元組,依次拷貝到其引數p代表的位元組切片中,並立即根據實際拷貝的位元組數增加已讀計數的值。
不過在另外一種情況下,其所屬值的已讀計數會等於已寫計數,這說明緩衝區中已經沒有任何未讀的位元組了。此時Read方法會先檢查引數p的長度是否大於或等於緩衝區的長度。
如果緩衝區中已無未讀位元組,引數p的長度大於或等於緩衝區的長度。那麼會放棄向緩衝區中填充資料,轉而直接從起底層讀取器讀出資料並拷貝到p中。這意味著它完全跨如果緩衝區,並直連了資料供需的雙方。
如果緩衝區中已無未讀位元組,緩衝區長度比引數p的長度更大。那麼會先把已讀計數和已寫計數的值都重置為0,然後再嘗試使用從底層讀取器裡獲取的資料,對緩衝區進行一次從頭至尾的填充。不過要注意,這裡的嘗試只會進行一次。無論在這一時刻是否能夠獲取到資料,也無論獲取是是否有錯誤發生。而這與fill方法的做法不同,只要沒有發生錯誤,fill方法就會進行多次嘗試,因此fill方法真正獲取到一些資料的可能性更大。所以Read方法中沒有呼叫fill方法,而是有一段自己的程式碼實現緩衝區的填充。而這兩個方法進行填充時的共同點是,只要把獲取到的資料寫入緩衝區,就會及時的更新已寫計數的值。
Read方法的使用示例:
package main import ( "bufio" "fmt" "os" "strings" ) func main() { comment := "Hello, World!" basicReader := strings.NewReader(comment) fmt.Println("字串長度:", basicReader.Size()) reader := bufio.NewReader(basicReader) buf := make([]byte, 5) n, err := reader.Read(buf) if err != nil { fmt.Fprintf(os.Stderr, "ERROE: %v\n", err) } fmt.Printf("Read讀取(%d): %q\n", n, buf) fmt.Println("緩衝區裡的未讀位元組數:", reader.Buffered()) }
ReadSlice方法
ReadSlice方法的功能是:持續的讀取資料,直至遇到呼叫方給定的分隔符為止。
ReadSlice方法,會先在緩衝區的未讀部分中尋找分隔符。如果未能找到,並且緩衝區未滿,那麼該方法會先通過呼叫fill方法對緩衝區進行填充,然後再次尋找。如果在填充過程中發生了錯誤(應該包括讀到結尾了返回EOF錯誤),那麼會把緩衝區中的未讀部分作為結果返回,同時返回相應的錯誤值。
在上面的過程中,可能會出現雖然緩衝區已填滿,但是仍然沒能找到分隔符的情況。ReadSlice方法會把緩衝區裡全部的內容返回,並返回緩衝區已滿的錯誤。此時的緩衝區是經過fill方法填充的,肯定從頭至尾都只包含未讀的位元組,所以這樣做是合理的。
如果ReadSlice方法找到了分隔符,就會在緩衝區上切除相應的、包含分隔符的位元組切片,並把該切片作為結果值返回。無論分隔符是否找到,該方法都會正確的設定已讀計數的值。
ReadSlice方法的使用示例:
package main import ( "bufio" "fmt" "os" "strings" ) func main() { comment := "Go is an open source programming language that makes it easy to build simple, " + "reliable," + "and efficient software." basicReader := strings.NewReader(comment) reader := bufio.NewReader(basicReader) delimiter := byte(',') line, err := reader.ReadSlice(delimiter) if err != nil { fmt.Fprintf(os.Stderr, "EEEOR: %v\n", err) } fmt.Printf("ReadSlice讀取(%d): %q\n", len(line), line) fmt.Println("緩衝區裡的未讀位元組數:", reader.Buffered()) fmt.Println() delimiter = byte('!')// 讀不到這個分隔符 line, err = reader.ReadSlice(delimiter) if err != nil { fmt.Fprintf(os.Stderr, "EEEOR: %v\n", err) } fmt.Printf("ReadSlice讀取(%d): %q\n", len(line), line) fmt.Println("緩衝區裡的未讀位元組數:", reader.Buffered()) fmt.Println() basicReader.Reset(comment) reader2 := bufio.NewReaderSize(basicReader, 80) delimiter = byte('!')// 讀不到這個分隔符 line, err = reader2.ReadSlice(delimiter) if err != nil { fmt.Fprintf(os.Stderr, "EEEOR: %v\n", err) } fmt.Printf("ReadSlice讀取(%d): %q\n", len(line), line) fmt.Println("緩衝區裡的未讀位元組數:", reader2.Buffered()) }
這個示例裡也演示了,讀完全部內容都沒有找到分隔符,以及緩衝區已滿並且其中沒有包含分隔符這兩種錯誤的情況。
ReadBytes方法
ReadBytes方法是基於ReadSlice方法實現的,它的內部會呼叫ReadSlice方法。
ReadSlice方法有一個問題,它是一個容易半途而廢的方法。它可能會因為緩衝區已滿而返回所有已讀到的位元組和相應的錯誤值,之後不會繼續尋找。而ReadBytes方法就相當執著,它會通過呼叫ReadSlice方法一次又一次的從緩衝區中讀取資料(原始碼裡是一個無限for迴圈呼叫ReadSlice方法),直至找到分隔符為止。在這個過程中,ReadSlice方法可能會因為緩衝區已滿而返回所有已讀到的位元組和響應的錯誤值,但ReadBytes方法會忽略掉這樣的錯誤,並再次呼叫ReadSlice方法,這樣就會繼續填充緩衝區並尋找分隔符。除非ReadSlice方法返回的錯誤值不是緩衝區已滿(errors.New("bufio: buffer full")
),或者它找到了分隔符(返回錯誤值nil),否則這個過程就不會結束(因為在無限for迴圈中)。等到尋找過程結束,ReadBytes方法會把這個過程中讀到的所有位元組,都返回。如果過程結束是因為出現錯誤,那麼第二個引數的錯誤值也會有內容返回。
ReadBytes方法的使用示例:
package main import ( "bufio" "fmt" "os" "strings" ) func main() { comment := "Go is an open source programming language that makes it easy to build simple, " + "reliable," + "and efficient software." basicReader := strings.NewReader(comment) reader := bufio.NewReaderSize(basicReader, 32) delimiter := byte(',') line, err := reader.ReadBytes(delimiter) if err != nil { fmt.Fprintf(os.Stderr, "EEEOR: %v\n", err) } fmt.Printf("ReadSlice讀取(%d): %q\n", len(line), line) fmt.Println("緩衝區裡的未讀位元組數:", reader.Buffered()) }
另外,bufio.Reader型別的ReadString方法完全依賴於這裡的ReadBytes方法。只是在返回值的時候做了一個簡單的型別轉換,轉成了字串型別。具體可以看原始碼:
func (b *Reader) ReadString(delim byte) (string, error) { bytes, err := b.ReadBytes(delim) return string(bytes), err }
ReadLine方法
在bufio.Reader型別的眾多讀取方法中,依賴ReadSlice方法的除了ReadBytes方法,還有ReadLine方法。這個方法是非常常用的一個方法,不過在讀取流程上並沒有什麼特別的地方。這裡就略了。
內容洩露
最後還有一個安全性的問題。bufio.Reader型別的Peek方法、ReadSlice方法和ReadLine方法都有可能造成內容洩露。主要是因為返回值是直接基於緩衝區的位元組切片。這個問題在bytes包裡已經提過了:呼叫方可以通過這些方法返回的介面值訪問到緩衝區的其他部分,甚至是修改緩衝區中的內容。
在簡單演示下獲取到後面的內容,獲取之後直接就可以操作擴張後的位元組切片把裡面的內容修改掉:
package main import ( "bufio" "fmt" "os" "strings" ) func main() { comment := "Test contents leak." basicReader := strings.NewReader(comment) reader := bufio.NewReaderSize(basicReader, 30) bytes, err := reader.Peek(5) if err != nil { fmt.Fprintf(os.Stderr, "ERROR: %v\n", err) } fmt.Printf("Peek讀取(%d): %q\n", len(bytes), bytes) // 擴張返回的位元組切片 bytes = bytes[:cap(bytes)] fmt.Printf("利用內容洩露獲取到了所有的內容: %q\n", bytes) }