golang bufio
當頻繁地對少量數據讀寫時會佔用IO,造成性能問題。golang的bufio
庫使用緩存來一次性進行大塊數據的讀寫,以此降低IO系統調用,提升性能。
在Transport中可以設置一個名爲WriteBufferSize
的參數,該參數指定了底層(Transport.dialConn
)寫buffer的大小。
tr := &http.Transport{
WriteBufferSize: 64 * 1024,
}
pconn.br = bufio.NewReaderSize(pconn, t.readBufferSize())
pconn.bw = bufio.NewWriterSize(persistConnWriter{pconn}, t.writeBufferSize())
使用bufio進行寫
可以使用bufio.NewWriter
初始化一個大小爲4096字節的Writer
(見下),或使用bufio.NewWriterSize
初始化一個指定大小的Writer
。
Writer
中的主要參數爲緩存區buf
,緩存區中的數據偏移量n
以及寫入接口wr
:
type Writer struct {
err error
buf []byte
n int
wr io.Writer
}
bufio.Writer
方法可以一次性寫入緩存中的數據,通常有如下三種情況:
- 緩存中滿數據
- 緩存中仍有空間
- 待寫入的數據大於緩存的大小
緩存中滿數據
當緩存中滿數據時,會執行寫操作。
緩存中仍有空間
如果緩存中仍有數據,則不會執行寫入動作,除非調用Flush()
方法。
待寫入的數據大於緩存的大小
由於此時緩存無法緩存足夠的數據,此時會跳過緩存直接執行寫操作
type Writer int
func (*Writer) Write(p []byte) (n int, err error) {
fmt.Printf("Writing: %s\n", p)
return len(p), nil
}
func main() {
w := new(Writer)
bw1 := bufio.NewWriterSize(w, 4)
// Case 1: Writing to buffer until full
bw1.Write([]byte{'1'})
bw1.Write([]byte{'2'})
bw1.Write([]byte{'3'})
bw1.Write([]byte{'4'}) // write - buffer is full
// Case 2: Buffer has space
bw1.Write([]byte{'5'}) //此時buffer中無法容納更多的數據,執行寫操作,寫入 []byte{'1','2','3','4'}
err = bw1.Flush() // forcefully write remaining
if err != nil {
panic(err)
}
// Case 3: (too) large write for buffer
// Will skip buffer and write directly
bw1.Write([]byte("12345")) //buffer不足,直接執行寫操作
}
//結果:
Writing: 1234
Writing: 5
Writing: 12345
緩存重用
申請緩存對性能是有損耗的,可以使用Reset
方法重置緩存,其內部只是將Writer
的數據偏移量n
置0。
wr := new(Writer)
bw := bufio.NewWriterSize(wr,2)
bw.Reset(wr)
獲取緩存的可用空間數
Available()
方法可以返回緩存的可用空間數,即len(Writer.buf)-Writer.n
使用bufio進行讀
與用於寫數據的Writer
類似,讀數據也有一個Reader
,可以使用NewReader
初始化一個大小爲4096字節的Reader
,或使用NewReaderSize
初始化一個指定大小的Reader
(要求最小字節爲16)。Reader
也有一個記錄偏移量的變量r
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 // last byte read for UnreadByte; -1 means invalid
lastRuneSize int // size of last rune read for UnreadRune; -1 means invalid
}
Peek
該方法會返回buf中的前n個字節的內容,但與Read操作不同的是,它不會消費緩存中的數據,即不會增加數據偏移量,因此通常也會用於判斷是否讀取結束(EOF)。通常有如下幾種情況:
- 如果peak的值小於緩存大小,則返回相應的內容
- 如果peak的值大於緩存大小,則返回bufio.ErrBufferFull錯誤
- 如果peak的值包含EOF且小於緩存大小,則返回EOF
Read
將數據讀取到p
,涉及將數據從緩存拷貝到p
。
func (b *Reader) Read(p []byte) (n int, err error)
ReadSlice
該方法會讀從緩存讀取數據,直到遇到第一個delim
。如果緩存中沒有delim
,則返回EOF,如果查詢的長度超過了緩存大小,則返回 io.ErrBufferFull
錯誤。
func (b *Reader) ReadSlice(delim byte) (line []byte, err error)
例如delim
爲','
,則下面會返回的內容爲1234,
。
func main() {
r := strings.NewReader("1234,567")
rb := bufio.NewReaderSize(r, 20)
fmt.Println(rb.ReadSlice(','))
}
// 結果:[49 50 51 52 44] <nil>
注意:
ReadSlice
返回的是原始緩存中的內容,如果針對緩存作併發操作,則返回的內容有可能被其他操作覆蓋。因此在官方註釋裏面有寫,建議使用ReadBytes
或ReadString
。但ReadBytes
和ReadString
涉及內存申請和拷貝,因此會影響性能。在追求高性能的場景下,建議外部使用sync.pool
來提供緩存。// Because the data returned from ReadSlice will be overwritten // by the next I/O operation, most clients should use // ReadBytes or ReadString instead.
ReadLine
ReadLine() (line []byte, isPrefix bool, err error)
ReadLine
底層用到了ReadSlice
,但在返回時會移除\n
或\r\n
。需要注意的是,如果切片中沒有找到換行符,則不會返回EOF或io.ErrBufferFull
錯誤,相反,它會將isPrefix
置爲true
ReadBytes
與ReadSlice
類似,但它會返回一個新的切片,因此便於併發使用。如果找不到delim
,ReadBytes
會返回io.EOF
func (b *Reader) ReadBytes(delim byte) ([]byte, error)
Scanner
scanner可以不斷將數據讀取到緩存(默認64*1024字節)。
func main() {
rb := strings.NewReader("12345678901234567890")
scanner := bufio.NewScanner(rb)
for scanner.Scan() {
fmt.Printf("Token (Scanner): %q\n", scanner.Text())
}
}
// 結果:Token (Scanner): "12345678901234567890"
附
併發複用緩存
io.bufio
支持緩存讀寫以及Reset
操作,但在併發複用緩存方面做的不是很好,可以參考:victoriaMetrics之byteBuffer。
無需併發複用的話,用
io.bufio
即可。
限制從io.Reader
中讀取的數據量
方式1
使用io.LimitReader
來限制從Reader
中讀取的數據量,LimitedReader.N
給出了可讀取的剩餘數據量。一旦N變爲0,即時Reader
中仍然有數據,此時也會返回EOF
type LimitedReader struct {
R Reader // underlying reader
N int64 // max bytes remaining
}
func main() {
rb := strings.NewReader("12345678901234567890")
lr := io.LimitReader(rb, 3)//限制可以讀取3個字節的數據
buf := make([]byte, 400)
fmt.Println(lr.Read(buf)) //達到讀取上限制,LimitedReader.N=0
fmt.Println(lr.Read(buf)) //此時返回EOF
}
//結果
3 <nil>
0 EOF
方式2
可以使用io.CopyN
限制從Reader
讀取的數據量,它內部也使用了io.LimitReader
,但支持多次讀取。
type Writer int
func (*Writer) Write(p []byte) (n int, err error) {
fmt.Printf("Writing: %s\n", p)
return len(p), nil
}
func main() {
rb := strings.NewReader("12345678901234567890")
w := new(Writer)
fmt.Println(io.CopyN(w, rb, 6))
fmt.Println(io.CopyN(w, rb, 6))
}
//結果
Writing: 123456
6 <nil>
Writing: 789012
6 <nil>