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

MingsonZheng發表於2021-12-05

43 | bufio包中的資料型別(下)

在上一篇文章中,我提到了bufio包中的資料型別主要有Reader、Scanner、Writer和ReadWriter。並著重講到了bufio.Reader型別與bufio.Writer型別,今天,我們繼續專注bufio.Reader的內容來進行學習。

知識擴充套件

問題 :bufio.Reader型別讀取方法有哪些不同?

bufio.Reader型別擁有很多用於讀取資料的指標方法,這裡面有 4 個方法可以作為不同讀取流程的代表,它們是:Peek、Read、ReadSlice和ReadBytes。

Reader值的Peek方法的功能是:讀取並返回其緩衝區中的n個未讀位元組,並且它會從已讀計數代表的索引位置開始讀。

在緩衝區未被填滿,並且其中的未讀位元組的數量小於n的時候,該方法就會呼叫fill方法,以啟動緩衝區填充流程。但是,如果它發現上次填充緩衝區的時候有錯誤,那就不會再次填充。

如果呼叫方給定的n比緩衝區的長度還要大,或者緩衝區中未讀位元組的數量小於n,那麼Peek方法就會把“所有未讀位元組組成的序列”作為第一個結果值返回。

同時,它通常還把“bufio.ErrBufferFull變數的值(以下簡稱緩衝區已滿的錯誤)”

作為第二個結果值返回,用來表示:雖然緩衝區被壓縮和填滿了,但是仍然滿足不了要求。

只有在上述的情況都沒有出現時,Peek方法才能返回:“以已讀計數為起始的n個位元組”和“表示未發生任何錯誤的nil”。

bufio.Reader型別的 Peek 方法有一個鮮明的特點,那就是:即使它讀取了緩衝區中的資料,也不會更改已讀計數的值。

這個型別的其他讀取方法並不是這樣。就拿該型別的Read方法來說,它有時會把緩衝區中的未讀位元組,依次拷貝到其引數p代表的位元組切片中,並立即根據實際拷貝的位元組數增加已讀計數的值。

  • 在緩衝區中還有未讀位元組的情況下,該方法的做法就是如此。不過,在另一些時候,其所屬值的已讀計數會等於已寫計數,這表明:此時的緩衝區中已經沒有任何未讀的位元組了。
  • 當緩衝區中已無未讀位元組時,Read方法會先檢查引數p的長度是否大於或等於緩衝區的長度。如果是,那麼Read方法會索性放棄向緩衝區中填充資料,轉而直接從其底層讀取器中讀出資料並拷貝到p中。這意味著它完全跨過了緩衝區,並直連了資料供需的雙方。

需要注意的是,Peek方法在遇到類似情況時的做法與這裡的區別(這兩種做法孰優孰劣還要看具體的使用場景)。

Peek方法會在條件滿足時填充緩衝區,並在發現引數n的值比緩衝區的長度更大時,直接返回緩衝區中的所有未讀位元組。

如果我們當初設定的緩衝區長度很大,那麼在這種情況下的方法執行耗時,就有可能會比較長。最主要的原因是填充緩衝區需要花費較長的時間。

由fill方法執行的流程可知,它會盡量填滿緩衝區中的可寫空間。然而,Read方法在大多數的情況下,是不會向緩衝區中寫入資料的,尤其是在前面描述的那種情況下,即:緩衝區中已無未讀位元組,且引數p的長度大於或等於緩衝區的長度。

此時,該方法會直接從底層讀取器那裡讀出資料,所以資料的讀出速度就成為了這種情況下方法執行耗時的決定性因素。

當然了,我在這裡說的只是耗時操作在某些情況下更可能出現在哪裡,一切的結論還是要以效能測試的客觀結果為準。

說回Read方法的內部流程。如果緩衝區中已無未讀位元組,但其長度比引數p的長度更大,那麼該方法會先把已讀計數和已寫計數的值都重置為0,然後再嘗試著使用從底層讀取器那裡獲取的資料,對緩衝區進行一次從頭至尾的填充。

不過要注意,這裡的嘗試只會進行一次。無論在這一時刻是否能夠獲取到資料,也無論獲取時是否有錯誤發生,都會是如此。而fill方法的做法與此不同,只要沒有發生錯誤,它就會進行多次嘗試,因此它真正獲取到一些資料的可能性更大。

不過,這兩個方法有一點是相同,那就是:只要它們把獲取到的資料寫入緩衝區,就會及時地更新已寫計數的值。

再來說ReadSlice方法和ReadBytes方法。 這兩個方法的功能總體上來說,都是持續地讀取資料,直至遇到呼叫方給定的分隔符為止。

ReadSlice方法會先在其緩衝區的未讀部分中尋找分隔符。如果未能找到,並且緩衝區未滿,那麼該方法會先通過呼叫fill方法對緩衝區進行填充,然後再次尋找,如此往復。

如果在填充的過程中發生了錯誤,那麼它會把緩衝區中的未讀部分作為結果返回,同時返回相應的錯誤值。

注意,在這個過程中有可能會出現雖然緩衝區已被填滿,但仍然沒能找到分隔符的情況。

這時,ReadSlice方法會把整個緩衝區(也就是buf欄位代表的位元組切片)作為第一個結果值,並把緩衝區已滿的錯誤(即bufio.ErrBufferFull變數的值)作為第二個結果值。

經過fill方法填滿的緩衝區肯定從頭至尾都只包含了未讀的位元組,所以這樣做是合理的。

當然了,一旦ReadSlice方法找到了分隔符,它就會在緩衝區上切出相應的、包含分隔符的位元組切片,並把該切片作為結果值返回。無論分隔符找到與否,該方法都會正確地設定已讀計數的值。

比如,在返回緩衝區中的所有未讀位元組,或者代表全部緩衝區的位元組切片之前,它會把已寫計數的值賦給已讀計數,以表明緩衝區中已無未讀位元組。

如果說ReadSlice是一個容易半途而廢的方法的話,那麼可以說ReadBytes方法算得上是相當的執著。

ReadBytes方法會通過呼叫ReadSlice方法一次又一次地從緩衝區中讀取資料,直至找到分隔符為止。

在這個過程中,ReadSlice方法可能會因緩衝區已滿而返回所有已讀到的位元組和相應的錯誤值,但ReadBytes方法總是會忽略掉這樣的錯誤,並再次呼叫ReadSlice方法,這使得後者會繼續填充緩衝區並在其中尋找分隔符。

除非ReadSlice方法返回的錯誤值並不代表緩衝區已滿的錯誤,或者它找到了分隔符,否則這一過程永遠不會結束。

如果尋找的過程結束了,不管是不是因為找到了分隔符,ReadBytes方法都會把在這個過程中讀到的所有位元組,按照讀取的先後順序組裝成一個位元組切片,並把它作為第一個結果值。如果過程結束是因為出現錯誤,那麼它還會把拿到的錯誤值作為第二個結果值。

在bufio.Reader型別的眾多讀取方法中,依賴ReadSlice方法的除了ReadBytes方法,還有ReadLine方法。不過後者在讀取流程上並沒有什麼特別之處,我就不在這裡贅述了。

另外,該型別的ReadString方法完全依賴於ReadBytes方法,前者只是在後者返回的結果值之上做了一個簡單的型別轉換而已。

最後,我還要提醒你一下,有個安全性方面的問題需要你注意。bufio.Reader型別的Peek方法、ReadSlice方法和ReadLine方法都有可能會造成內容洩露。

這主要是因為它們在正常的情況下都會返回直接基於緩衝區的位元組切片。我在講bytes.Buffer型別的時候解釋過什麼叫內容洩露。你可以返回檢視。

呼叫方可以通過這些方法返回的結果值訪問到緩衝區的其他部分,甚至修改緩衝區中的內容。這通常都是很危險的。

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())

	size := 300
	fmt.Printf("New a buffered reader with size %d ...\n", size)
	reader1 := bufio.NewReaderSize(basicReader, size)
	fmt.Println()

	fmt.Print("[ About 'Peek' method ]\n\n")
	// 示例1。
	peekNum := 38
	fmt.Printf("Peek %d bytes ...\n", peekNum)
	bytes, err := reader1.Peek(peekNum)
	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()

	fmt.Print("[ About 'Read' method ]\n\n")
	// 示例2。
	readNum := 38
	buf1 := make([]byte, readNum)
	fmt.Printf("Read %d bytes ...\n", readNum)
	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()

	fmt.Print("[ About 'ReadSlice' method ]\n\n")
	// 示例3。
	fmt.Println("Reset the basic reader ...")
	basicReader.Reset(comment)
	fmt.Println("Reset the buffered reader ...")
	reader1.Reset(basicReader)
	fmt.Println()

	delimiter := byte('(')
	fmt.Printf("Read slice with delimiter %q...\n", delimiter)
	line, err := reader1.ReadSlice(delimiter)
	if err != nil {
		fmt.Printf("error: %v\n", err)
	}
	fmt.Printf("Read contents(%d): %q\n", len(line), line)
	fmt.Printf("The number of unread bytes in the buffer: %d\n", reader1.Buffered())
	fmt.Println()

	delimiter = byte('[')
	fmt.Printf("Read slice with delimiter %q...\n", delimiter)
	line, err = reader1.ReadSlice(delimiter)
	if err != nil {
		fmt.Printf("error: %v\n", err)
	}
	fmt.Printf("Read contents(%d): %q\n", len(line), line)
	fmt.Printf("The number of unread bytes in the buffer: %d\n", reader1.Buffered())
	fmt.Println()

	// 示例4。
	fmt.Println("Reset the basic reader ...")
	basicReader.Reset(comment)
	size = 200
	fmt.Printf("New a buffered reader with size %d ...\n", size)
	reader2 := bufio.NewReaderSize(basicReader, size)
	fmt.Println()

	delimiter = byte('[')
	fmt.Printf("Read slice with delimiter %q...\n", delimiter)
	line, err = reader2.ReadSlice(delimiter)
	if err != nil {
		fmt.Printf("error: %v\n", err)
	}
	fmt.Printf("Read contents(%d): %q\n", len(line), line)
	fmt.Printf("The number of unread bytes in the buffer: %d\n", reader2.Buffered())
	fmt.Println()

	fmt.Print("[ About 'ReadBytes' method ]\n\n")
	// 示例5。
	fmt.Println("Reset the basic reader ...")
	basicReader.Reset(comment)
	size = 200
	fmt.Printf("New a buffered reader with size %d ...\n", size)
	reader3 := bufio.NewReaderSize(basicReader, size)
	fmt.Println()

	delimiter = byte('[')
	fmt.Printf("Read bytes with delimiter %q...\n", delimiter)
	line, err = reader3.ReadBytes(delimiter)
	if err != nil {
		fmt.Printf("error: %v\n", err)
	}
	fmt.Printf("Read contents(%d): %q\n", len(line), line)
	fmt.Printf("The number of unread bytes in the buffer: %d\n", reader3.Buffered())
	fmt.Println()

	// 示例6和示例7。
	fmt.Print("[ About contents leak ]\n\n")
	showContentsLeak(comment)
}

func showContentsLeak(comment string) {
	// 示例6。
	basicReader := strings.NewReader(comment)
	fmt.Printf("The size of basic reader: %d\n", basicReader.Size())

	size := len(comment)
	fmt.Printf("New a buffered reader with size %d ...\n", size)
	reader4 := bufio.NewReaderSize(basicReader, size)
	fmt.Println()

	peekNum := 7
	fmt.Printf("Peek %d bytes ...\n", peekNum)
	bytes, err := reader4.Peek(peekNum)
	if err != nil {
		fmt.Printf("error: %v\n", err)
	}
	fmt.Printf("Peeked contents(%d): %q\n", len(bytes), bytes)
	fmt.Println()

	// 只要擴充一下之前拿到的位元組切片bytes,
	// 就可以用它來讀取甚至修改緩衝區中的後續內容。
	bytes = bytes[:cap(bytes)]
	fmt.Printf("The all of the contents in the buffer:\n%q\n", bytes)
	fmt.Println()

	blank := byte(' ')
	fmt.Println("Set blanks into the contents in the buffer ...")
	for _, i := range []int{55, 56, 57, 58, 66, 67, 68} {
		bytes[i] = blank
	}
	fmt.Println()

	peekNum = size
	fmt.Printf("Peek %d bytes ...\n", peekNum)
	bytes, err = reader4.Peek(peekNum)
	if err != nil {
		fmt.Printf("error: %v\n", err)
	}
	fmt.Printf("Peeked contents(%d):\n%q\n", len(bytes), bytes)
	fmt.Println()

	// 示例7。
	// ReadSlice方法也存在相同的問題。
	delimiter := byte(',')
	fmt.Printf("Read slice with delimiter %q...\n", delimiter)
	line, err := reader4.ReadSlice(delimiter)
	if err != nil {
		fmt.Printf("error: %v\n", err)
	}
	fmt.Printf("Read contents(%d): %q\n", len(line), line)
	fmt.Println()

	line = line[:cap(line)]
	fmt.Printf("The all of the contents in the buffer:\n%q\n", line)
	fmt.Println()

	underline := byte('_')
	fmt.Println("Set underlines into the contents in the buffer ...")
	for _, i := range []int{89, 92, 103} {
		line[i] = underline
	}
	fmt.Println()

	peekNum = size
	fmt.Printf("Peek %d bytes ...\n", peekNum)
	bytes, err = reader4.Peek(peekNum)
	if err != nil {
		fmt.Printf("error: %v\n", err)
	}
	fmt.Printf("Peeked contents(%d): %q\n", len(bytes), bytes)
}

總結

我們用比較長的篇幅介紹了bufio包中的資料型別,其中的重點是bufio.Reader型別。

bufio.Reader型別代表的是攜帶緩衝區的讀取器。它的值在被初始化的時候需要接受一個底層的讀取器,後者的型別必須是io.Reader介面的實現。

Reader值中的緩衝區其實就是一個資料儲存中介,它介於底層讀取器與讀取方法及其呼叫方之間。此類值的讀取方法一般都會先從該值的緩衝區中讀取資料,同時在必要的時候預先從其底層讀取器那裡讀出一部分資料,並填充到緩衝區中以備後用。填充緩衝區的操作通常會由該值的fill方法執行。

在填充的過程中,fill方法有時還會對緩衝區進行壓縮。在Reader值擁有的眾多讀取方法中,有 4 個方法可以作為不同讀取流程的代表,它們是:Peek、Read、ReadSlice和ReadBytes。

Peek方法的特點是即使讀取了緩衝區中的資料,也不會更改已讀計數的值。而Read方法會在引數值的長度過大,且緩衝區中已無未讀位元組時,跨過緩衝區並直接向底層讀取器索要資料。

ReadSlice方法會在緩衝區的未讀部分中尋找給定的分隔符,並在必要時對緩衝區進行填充。

如果在填滿緩衝區之後仍然未能找到分隔符,那麼該方法就會把整個緩衝區作為第一個結果值返回,同時返回緩衝區已滿的錯誤。

ReadBytes方法會通過呼叫ReadSlice方法,一次又一次地填充緩衝區,並在其中尋找分隔符。除非發生了未預料到的錯誤或者找到了分隔符,否則這一過程將會一直進行下去。

Reader值的ReadLine方法會依賴於它的ReadSlice方法,而其ReadString方法則完全依賴於ReadBytes方法。

另外,值得我們特別注意的是,Reader值的Peek方法、ReadSlice方法和ReadLine方法都可能會造成其緩衝區中的內容的洩露。

最後再說一下bufio.Writer型別。把該類值的緩衝區中暫存的資料寫進其底層寫入器的功能,主要是由它的Flush方法實現的。

此類值的所有資料寫入方法都會在必要的時候呼叫它的Flush方法。一般情況下,這些寫入方法都會先把資料寫進其所屬值的緩衝區,然後再增加該值中的已寫計數。但是,在有些時候,Write方法和ReadFrom方法也會跨過緩衝區,並直接把資料寫進其底層寫入器。

請記住,雖然這些寫入方法都會不時地呼叫Flush方法,但是在寫入所有的資料之後再顯式地呼叫一下這個方法總是最穩妥的。

package main

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

func main() {
	comment := "Writer implements buffering for an io.Writer object. " +
		"If an error occurs writing to a Writer, " +
		"no more data will be accepted and all subsequent writes, " +
		"and Flush, will return the error. After all data has been written, " +
		"the client should call the Flush method to guarantee all data " +
		"has been forwarded to the underlying io.Writer."
	basicWriter1 := &strings.Builder{}

	size := 300
	fmt.Printf("New a buffered writer with size %d ...\n", size)
	writer1 := bufio.NewWriterSize(basicWriter1, size)
	fmt.Println()

	// 示例1。
	begin, end := 0, 53
	fmt.Printf("Write %d bytes into the writer ...\n", end-begin)
	writer1.WriteString(comment[begin:end])
	fmt.Printf("The number of buffered bytes: %d\n", writer1.Buffered())
	fmt.Printf("The number of unused bytes in the buffer: %d\n",
		writer1.Available())
	fmt.Println("Flush the buffer in the writer ...")
	writer1.Flush()
	fmt.Printf("The number of buffered bytes: %d\n", writer1.Buffered())
	fmt.Printf("The number of unused bytes in the buffer: %d\n",
		writer1.Available())
	fmt.Println()

	// 示例2。
	begin, end = 0, 326
	fmt.Printf("Write %d bytes into the writer ...\n", end-begin)
	writer1.WriteString(comment[begin:end])
	fmt.Printf("The number of buffered bytes: %d\n", writer1.Buffered())
	fmt.Printf("The number of unused bytes in the buffer: %d\n",
		writer1.Available())
	fmt.Println("Flush the buffer in the writer ...")
	writer1.Flush()
	fmt.Println()

	// 示例3。
	basicWriter2 := &bytes.Buffer{}
	fmt.Printf("Reset the writer with a bytes buffer(an implementation of io.ReaderFrom) ...\n")
	writer1.Reset(basicWriter2)
	reader := strings.NewReader(comment)
	fmt.Println("Read data from the reader ...")
	writer1.ReadFrom(reader)
	fmt.Printf("The number of buffered bytes: %d\n", writer1.Buffered())
	fmt.Printf("The number of unused bytes in the buffer: %d\n",
		writer1.Available())
}

思考題

今天的思考題是:bufio.Scanner型別的主要功用是什麼?它有哪些特點?

筆記原始碼

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

知識共享許可協議

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

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

相關文章