您好,登錄后才能下訂單哦!
這是另一個與I/O操作強相關的代碼包。bufio是“buffered I/O”的縮寫,這個代碼包中的程序實體實現的I/O操作都內置了緩沖區。
bufio包中的數據類型主要有:
與io包中的數據類型類似,這些類型的值也都需要在初始化的時候,包裝一個或多個簡單I/O接口類型的值。這里的簡單I/O接口類型指的就是io包中的那些簡單接口。
bufio.Reader類型的值內的緩沖區其實就是一個數據存儲中介,它介于底層讀取器與讀取方法及其調用方之間。所謂的底層讀取器,就是在初始化此類值的時候傳入的io.Reader類型的參數值。
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
}
簡要的解釋一下結構體中的字段:
bufio包提供了兩個用于用于初始化Reader值的函數,都會返回一個*bufio.Reader類型的值:
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函數就提供了這樣一個途徑。
在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類型有一個Flush方法,它的主要功能是把相應緩沖區中暫存的所有數據,都寫到底層寫入器中。數據一旦被寫進底層寫入器,該方法就會把這些數據從緩沖區中刪除掉。這里的刪除有時候只是邏輯上的刪除而已。不論是否成功的寫入了所有的暫存數據,Flush方法都會妥當處置,并保證不會出現重寫和漏寫的情況。該類型的字段n在此會起到很重要的作用。
bufio.Writer結構體的定義如下:
type Writer struct {
err error
buf []byte
n int
wr io.Writer
}
字段說明:
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方法的功能是:讀取并返回其緩沖區中的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方法,在緩沖區中還有未讀字節的情況下,它會把緩沖區中的未讀字節,依次拷貝到其參數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方法,會先在緩沖區的未讀部分中尋找分隔符。如果未能找到,并且緩沖區未滿,那么該方法會先通過調用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方法是基于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
}
在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)
}
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。