golang bufio解析

charlieroro發表於2022-04-21

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方法可以一次性寫入快取中的資料,通常有如下三種情況:

  1. 快取中滿資料
  2. 快取中仍有空間
  3. 待寫入的資料大於快取的大小

快取中滿資料

當快取中滿資料時,會執行寫操作。

快取中仍有空間

如果快取中仍有資料,則不會執行寫入動作,除非呼叫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)。通常有如下幾種情況:

  1. 如果peak的值小於快取大小,則返回相應的內容
  2. 如果peak的值大於快取大小,則返回bufio.ErrBufferFull錯誤
  3. 如果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返回的是原始快取中的內容,如果針對快取作併發操作,則返回的內容有可能被其他操作覆蓋。因此在官方註釋裡面有寫,建議使用ReadBytesReadString。但ReadBytesReadString涉及記憶體申請和拷貝,因此會影響效能。在追求高效能的場景下,建議外部使用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類似,但它會返回一個新的切片,因此便於併發使用。如果找不到delimReadBytes會返回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>

參考

how-to-read-and-write-with-golang-bufio

相關文章