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
rd io.Reader // reader provided by the client
r, w int // buf read and write positions
err error
lastByte int
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
n int
wr io.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)
}