Go 語言 bytes-Buffer 源碼詳解之 1
關注不迷路,一起 From Zero To Hero !
前言
前面一篇文章 Go 語言 strings.Reader 源碼詳解,我們對 strings
包中的 Reader
結構進行了詳細的分析,今天我們來學習 bytes
包中的 Buffer
結構。bytes 包與 strings 包 可以說是一對孿生兄弟,從包名稱可以看出,strings 包主要是對字符串進行操作,而 bytes 包面向的主要是字節和字節切片。
bytes.Reader
與 strings.Reader
的功能和實現基本類似,完全可以類比學習,本篇文章就來學習一個新的結構:bytes.Buffer
。從名稱可以看出,bytes.Buffer 是一個緩衝區(buffer),更具體點來說,bytes.Buffer 是一個集讀寫於一體、緩衝區大小可變
的字節緩衝區,下面我們就來一探究竟吧!
初體驗
我們首先來體驗下 bytes.Buffer 的使用。
-
首先我們聲明瞭一個 buffer 變量,然後調用 WriteString() 方法往緩衝區內寫入了一個字符串,返回值爲 31,nil,表示寫入的字節長度和產生的 error
-
然後我們想打印出緩衝區的長度和容量,調用了 Len() 和 Cap() 方法,返回了 31 和 64,這和我們的認知應該相符,畢竟我們寫入了字節長度爲 31 的字符串,同時可能有擴容策略,容量爲 64
-
接下來我們調用 Read() 方法讀取數據,將數據讀入了字節切片中,同時打印出了讀取的數據及長度,和寫入的均相符
-
最後我們再次調用 Len() 和 Cap() 方法,發現返回的長度和容量分別爲 0 和 64,那麼爲什麼長度會變成 0,而容量卻沒變呢?帶着這個疑問,我們一起來學習下 bytes.Buffer 的實現吧!
var buffer bytes.Buffer
n, err := buffer.WriteString("this is a test for bytes buffer")
fmt.Println(n, err) // 31 nil
fmt.Println(buffer.Len(), buffer.Cap()) // 31 64
s := make([]byte, 1000)
n, err = buffer.Read(s)
fmt.Println(n, err) // 31 nil
fmt.Println(string(s)) // this is a test for bytes buffer
fmt.Println(buffer.Len(), buffer.Cap()) // 0 64
結構定義
Buffer 是集讀寫功能於一身,緩衝區大小可變的字節緩衝區,結構中有如下三個變量:
-
buf: 底層的緩衝字節切片,用於保存數據。len(buf) 表示字節切片長度,cap(buf) 表示切片容量
-
off: 已讀計數,在該位置之前的數據都是被讀取過的,off 表示下次讀取時的開始位置。因此未讀數據部分爲 buf[off:len(buf)]
-
lastRead: 保存上次的讀操作類型,用於後續的回退操作
type Buffer struct {
buf []byte
off int
lastRead readOp
}
Buffer 結構示意圖
下面是 bytes.Buffer 中定義的一些常量:
// 初始化底層緩衝字節數組容量時,分配的最小值
const smallBufferSize = 64
// readOp 常量表示上次的操作類型,用於後續使用 UnreadRune 和 UnreadByte 回退時檢查操作是否合法
// 有四種 opReadRuneX,表示上次讀 rune 時對應的字節大小
type readOp int8
const (
opRead readOp = -1 // 任意讀操作
opInvalid readOp = 0 // 非讀操作
opReadRune1 readOp = 1 // 長度爲 1 的 rune
opReadRune2 readOp = 2 // 長度爲 2 的 rune
opReadRune3 readOp = 3 // 長度爲 3 的 rune
opReadRune4 readOp = 4 // 長度爲 4 的 rune
)
// 在擴容時會用到,如果緩衝字節切片太大,內存不夠分配時會panic,並給出該提示
var ErrTooLarge = errors.New("bytes.Buffer: too large")
// 讀到的數據量爲負值時提示該錯誤
var errNegativeRead = errors.New("bytes.Buffer: reader returned negative count from Read")
// 緩衝字節切片的最大容量
const maxInt = int(^uint(0) >> 1)
方法定義
Bytes()
-
Bytes() 方法返回未讀的字節數據,即從已讀計數 off 開始,到 len(off) 結束,也就是上圖中的綠色部分。
-
由於返回的是字節切片,存在內容泄露的風險,因爲通過切片,我們可以直接訪問和操縱它的底層數組,不論這個切片是基於某個數組得來的,還是通過對另一個切片做切片操作獲得的。
-
同時,由於返回的是從 off 位置開始的切片,因此得到的數據是有
有效期
的。如果調用 Read()、Write()、 Reset()、 Truncate() 等類似會修改 off 變量值的方法,Bytes() 方法得到的數據就失效了。
func (b *Buffer) Bytes() []byte { return b.buf[b.off:] }
String()
String() 方法返回未讀數據的字符串的形式,不會存在內容泄露的風險。
func (b *Buffer) String() string {
if b == nil {
// Special case, useful in debugging.
return "<nil>"
}
return string(b.buf[b.off:])
}
empty()
empty() 方法返回是否還有未讀數據,即上圖中的綠色部分。如果已讀計數 off>= len(b.buf) ,說明沒有未讀數據了,返回 true
func (b *Buffer) empty() bool { return len(b.buf) <= b.off }
Len()
Len() 方法返回未讀數據部分的長度,即上圖綠色部分的長度。Bytes() 方法返回的是未讀部分的數據,即 b.Len() == len(b.Bytes())
func (b *Buffer) Len() int { return len(b.buf) - b.off }
Cap()
Cap() 方法返回底層緩衝字節切片 buf 的容量,由於底層的緩衝切片會擴容,因此該值是可變的。
func (b *Buffer) Cap() int { return cap(b.buf) }
Reset()
Reset() 重置整個結構,把緩衝字節切片長度修改爲 0,已讀計數設置爲 0,相當於上圖中的灰色已讀數據部分與綠色未讀數據部分長度均被設置爲 0。
雖然緩衝區 buf 底層數組中的數據沒有清空,但對於結構來說,通過 off 字段的控制,這些數據都是不可見的,讀取不到數據,後續再寫入數據會直接覆蓋這些髒數據。
func (b *Buffer) Reset() {
b.buf = b.buf[:0]
b.off = 0
b.lastRead = opInvalid
}
Truncate()
Truncate 會 保留未讀部分前n個字節
的數據,丟棄其餘部分,即只保留上圖綠色部分的 前 n 個 字節。
該方法後只是修改緩衝切片的長度 len(buf),因爲有效數據部分是 buf[off:len(buf)]
func (b *Buffer) Truncate(n int) {
// 對我們有用的數據只有未讀數據,如果 n==0,說明不需要保留未讀數據了
// 不保留相當於緩衝字節切片的數據都沒用了,直接重置
if n == 0 {
b.Reset()
return
}
// 設置上次操作類型
b.lastRead = opInvalid
// 如果要保留的長度小於0,或者 保留的長度大於未讀數據的長度,不合法,直接panic
if n < 0 || n > b.Len() {
panic("bytes.Buffer: truncation out of range")
}
// 保留n個未讀字節,也就是直接修改切片長度 len
b.buf = b.buf[:b.off+n]
}
tryGrowByReslice()
在向緩衝切片中寫 n 個字節之前,我們要確保至少有 n 個空白位置可以存放數據。從下圖可以看出,在 len(buf)
到 cap(buf)
之間本身就有空閒部分,如果 cap(buf) - len(buf) >= n
,說明空閒部分可以寫入 n 個字節,那麼我們就可以將 len(buf) 後移 n 位,將新增數據保存在這 n 個位置中。否則的話,就需要進行數據平移甚至擴容了,這些工作是下一個要介紹的 grow() 方法要做的事情,因此我們可以說 tryGrowByReslice() 是 grow() 的快速情況(fase-case),在成本最低的情況下滿足需求。在後續介紹相關的寫方法中我們會看到,調用 grow() 方法前都會先嚐試調用下 tryGrowByReslice(),不成功的話纔會調用 grow()。
tryGrowByReslice 示意圖
需要注意的是,如果本次操作成功,字節切片 buf 的長度被增大了,但是新增的 n 個字節還沒有數據,只是空出來了,用於調用者直接填充數據。
-
入參 n: 表示要增長的字節長度
-
返回值:增長後寫入數據的起始位置(調整前的 len(buf));本次快速增長是否成功
func (b *Buffer) tryGrowByReslice(n int) (int, bool) {
// 判斷容量與長度的差額,是否大於要增長的長度n,如果大於則滿足增長需求
if l := len(b.buf); n <= cap(b.buf)-l {
// 修改buf 的長度
b.buf = b.buf[:l+n]
// 寫入的起始位置爲l,本次操作成功
return l, true
}
// 快速增長失敗
return 0, false
}
grow()
grow() 通過對緩衝字節切片進行調整,甚至進行擴容,來確保有 n 個空閒位置供調用者寫入,方法返回寫入的開始位置。如果在擴容中,緩衝切片長度超過最大長度,會產生 ErrTooLarge 的 panic。
- 先進行數據整理,如果 buf 中沒有未讀數據,且已讀計數大於 0,重置,此時的整個緩衝切片都是空閒的,如下圖:
重置情況示意圖
-
調用 tryGrowByReslice,判斷通過 fast-case 是否滿足需求,如果滿足直接返回了,不滿足再進行下一步。
-
當前的 buf 可能還沒有初始化(聲明變量後,直接調用 Grow() 方法,手動擴容),如果 buf == nil,判斷最小緩衝大小是否滿足需求,滿足需求的話,創建一個字節切片返回即可。
-
數據平移。考慮下面這種情況,如果
未讀數據的長度 + 所需字節數 n <= 緩衝切片容量 cap(buf)
,可以將未讀數據平移到 buf 的頂端,覆蓋已讀數據,這樣就可以至少留出來 n 個字節了。
數據平移示意圖 1
可是在實際的源碼實現中,條件更加嚴苛點,要求 未讀數據的長度 + 所需字節數 n <= cap(buf)/2
,即兩者加起來要小於一半的容量,這樣做的原因是爲了防止頻繁的數據複製。
數據平移示意圖 2
-
擴容。上面的條件都不滿足,只能擴容,
新容器的容量 = 2 * 原有容量 + 所需字節數
。然後將原緩衝切片中的未讀數據,拷貝到新的緩衝切片頭部。 -
方法最後設置已讀計數爲 0,設置緩衝切片的長度爲
未讀數據長度 + 所需字節數 n
func (b *Buffer) grow(n int) int {
// m: 當前未讀字節的數量
m := b.Len()
// 未讀數據爲0,且off!=0,說明off位置之前的數據已經沒用了,白白佔用空間,可以首先 Reset 重置,
if m == 0 && b.off != 0 {
b.Reset()
}
// 通過reslice 的方式,判斷當前 len到cap部分 的空餘空間,是否滿足數據需求
if i, ok := b.tryGrowByReslice(n); ok {
return i
}
// 初始化結構體的時候,可能當前的 buf 是 nil,如果當前 buf 是 nil,且需要的空間小於定義的最小緩衝大小,
// 那麼就初始化緩衝數組容量爲smallBufferSize,長度爲 n
if b.buf == nil && n <= smallBufferSize {
b.buf = make([]byte, n, smallBufferSize)
return 0
}
// 上面的一些快速滿足的方式,如果都達不到要求,那麼下面就需要通過整理數據,或者重新分配內存的方式,來滿足需求:
c := cap(b.buf)
// 數據平移,將所有的有用數據,平移到緩衝切片頭部,類似於數據整理
// 按理來說,當 未讀數據m + 需要新增字節數n < 切片容量 c時,就可以完成平移,但是爲了防止下次再次grow時,頻繁的數據拷貝,設置的條件爲 m+n < n/2
if n <= c/2-m {
copy(b.buf, b.buf[b.off:])
} else if c > maxInt-c-n { // 重新分配內存的大小爲 2*切片容量c + 新增容量 n,如果需要重新分配的大小超出了最大容量,直接panic
panic(ErrTooLarge)
} else {
// 重新分配內存,然後將之前的數據拷貝到新的切片中
buf := makeSlice(2*c + n)
copy(buf, b.buf[b.off:])
// 新的切片作爲緩衝切片
b.buf = buf
}
// 重置已讀計數爲0,同時長度設置爲 m+n。
// 需要注意的是,[0,m)這段數據是歷史數據,[m,n)沒有數據,是空餘出來給調用方放數據的,如果調用方不需要放數據,需要修改buf的len,可以參考 Grow方法
b.off = 0
b.buf = b.buf[:m+n]
// 返回寫數據的開始位置
return m
}
makeSlice()
創建一個容量爲 n 的字節切片,如果分配失敗,產生 ErrTooLarge 的 panic,grow() 方法調用到了該方法。
//
func makeSlice(n int) []byte {
// If the make fails, give a known error.
defer func() {
if recover() != nil {
panic(ErrTooLarge)
}
}()
return make([]byte, n)
}
Grow()
對外暴露的用於手動擴容的方法。Grow() 通過調整底層的緩衝切片,確保可寫入 n 個字節的數據。
func (b *Buffer) Grow(n int) {
// 如果 n<0,會直接panic
if n < 0 {
panic("bytes.Buffer.Grow: negative count")
}
// m 是下次寫入的開始位置,根據 grow 方法,當前 buf 的長度爲 m+n,由於不需要寫數據,更新buf 的長度爲 m
m := b.grow(n)
b.buf = b.buf[:m]
}
總結
本篇文章我們學習了 bytes.Buffer 的結構定義和基礎方法源碼實現,其中最重要的是要記住 off 表示已讀計數。通過下圖,就能夠更容易理解相關方法的實現原理。
Buffer 結構示意圖
更多
個人博客: https://lifelmy.github.io/
微信公衆號:漫漫 Coding 路
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/BJ1fu9AwE2w7Ovg2C8pVbw