Go語言核心36講(Go語言實戰與應用十六)–學習筆記
- 2021 年 11 月 28 日
- 筆記
- 【016】Go語言核心36講
38 | bytes包與位元組串操作(上)
前導內容: bytes.Buffer基礎知識
strings包和bytes包可以說是一對孿生兄弟,它們在 API 方面非常的相似。單從它們提供的函數的數量和功能上講,差別可以說是微乎其微。
只不過,strings包主要面向的是 Unicode 字元和經過 UTF-8 編碼的字元串,而bytes包面對的則主要是位元組和位元組切片。
我今天會主要講bytes包中最有特色的類型Buffer。顧名思義,bytes.Buffer類型的用途主要是作為位元組序列的緩衝區。
與strings.Builder類型一樣,bytes.Buffer也是開箱即用的。
但不同的是,strings.Builder只能拼接和導出字元串,而bytes.Buffer不但可以拼接、截斷其中的位元組序列,以各種形式導出其中的內容,還可以順序地讀取其中的子序列。
可以說,bytes.Buffer是集讀、寫功能於一身的數據類型。當然了,這些也基本上都是作為一個緩衝區應該擁有的功能。
在內部,bytes.Buffer類型同樣是使用位元組切片作為內容容器的。並且,與strings.Reader類型類似,bytes.Buffer有一個int類型的欄位,用於代表已讀位元組的計數,可以簡稱為已讀計數。
不過,這裡的已讀計數就無法通過bytes.Buffer提供的方法計算出來了。
我們先來看下面的程式碼
var buffer1 bytes.Buffer
contents := "Simple byte buffer for marshaling data."
fmt.Printf("Writing contents %q ...\n", contents)
buffer1.WriteString(contents)
fmt.Printf("The length of buffer: %d\n", buffer1.Len())
fmt.Printf("The capacity of buffer: %d\n", buffer1.Cap())
我先聲明了一個bytes.Buffer類型的變數buffer1,並寫入了一個字元串。然後,我想列印出這個bytes.Buffer類型的值(以下簡稱Buffer值)的長度和容量。在運行這段程式碼之後,我們將會看到如下的輸出:
Writing contents "Simple byte buffer for marshaling data." ...
The length of buffer: 39
The capacity of buffer: 64
乍一看這沒什麼問題。長度39和容量64的含義看起來與我們已知的概念是一致的。我向緩衝區中寫入了一個長度為39的字元串,所以buffer1的長度就是39。
根據切片的自動擴容策略,64這個數字也是合理的。另外,可以想像,這時的已讀計數的值應該是0,這是因為我還沒有調用任何用於讀取其中內容的方法。
可實際上,與strings.Reader類型的Len方法一樣,buffer1的Len方法返回的也是內容容器中未被讀取部分的長度,而不是其中已存內容的總長度(以下簡稱內容長度)。示例如下:
p1 := make([]byte, 7)
n, _ := buffer1.Read(p1)
fmt.Printf("%d bytes were read. (call Read)\n", n)
fmt.Printf("The length of buffer: %d\n", buffer1.Len())
fmt.Printf("The capacity of buffer: %d\n", buffer1.Cap())
當我從buffer1中讀取一部分內容,並用它們填滿長度為7的位元組切片p1之後,buffer1的Len方法返回的結果值也會隨即發生變化。如果運行這段程式碼,我們會發現,這個緩衝區的長度已經變為了32。
另外,因為我們並沒有再向該緩衝區中寫入任何內容,所以它的容量會保持不變,仍是64。
總之,在這裡,你需要記住的是,Buffer值的長度是未讀內容的長度,而不是已存內容的總長度。 它與在當前值之上的讀操作和寫操作都有關係,並會隨著這兩種操作的進行而改變,它可能會變得更小,也可能會變得更大。
而Buffer值的容量指的是它的內容容器(也就是那個位元組切片)的容量,它只與在當前值之上的寫操作有關,並會隨著內容的寫入而不斷增長。
再說已讀計數。由於strings.Reader還有一個Size方法可以給出內容長度的值,所以我們用內容長度減去未讀部分的長度,就可以很方便地得到它的已讀計數。
然而,bytes.Buffer類型卻沒有這樣一個方法,它只有Cap方法。可是Cap方法提供的是內容容器的容量,也不是內容長度。
並且,這裡的內容容器容量在很多時候都與內容長度不相同。因此,沒有了現成的計算公式,只要遇到稍微複雜些的情況,我們就很難估算出Buffer值的已讀計數。
一旦理解了已讀計數這個概念,並且能夠在讀寫的過程中,實時地獲得已讀計數和內容長度的值,我們就可以很直觀地了解到當前Buffer值各種方法的行為了。不過,很可惜,這兩個數字我們都無法直接拿到。
雖然,我們無法直接得到一個Buffer值的已讀計數,並且有時候也很難估算它,但是我們絕對不能就此作罷,而應該通過研讀bytes.Buffer和文檔和源碼,去探究已讀計數在其中起到的關鍵作用。
否則,我們想用好bytes.Buffer的意願,恐怕就不會那麼容易實現了。
下面的這個問題,如果你認真地閱讀了bytes.Buffer的源碼之後,就可以很好地回答出來。
我們今天的問題是:bytes.Buffer類型的值記錄的已讀計數,在其中起到了怎樣的作用?
這道題的典型回答是這樣的。
bytes.Buffer中的已讀計數的大致功用如下所示。
- 讀取內容時,相應方法會依據已讀計數找到未讀部分,並在讀取後更新計數。
- 寫入內容時,如需擴容,相應方法會根據已讀計數實現擴容策略。
- 截斷內容時,相應方法截掉的是已讀計數代表索引之後的未讀部分。
- 讀回退時,相應方法需要用已讀計數記錄回退點。
- 重置內容時,相應方法會把已讀計數置為0。
- 導出內容時,相應方法只會導出已讀計數代表的索引之後的未讀部分。
- 獲取長度時,相應方法會依據已讀計數和內容容器的長度,計算未讀部分的長度並返回。
問題解析
通過上面的典型回答,我們已經能夠體會到已讀計數在bytes.Buffer類型,及其方法中的重要性了。沒錯,bytes.Buffer的絕大多數方法都用到了已讀計數,而且都是非用不可。
在讀取內容的時候,相應方法會先根據已讀計數,判斷一下內容容器中是否還有未讀的內容。如果有,那麼它就會從已讀計數代表的索引處開始讀取。
在讀取完成後,它還會及時地更新已讀計數。也就是說,它會記錄一下又有多少個位元組被讀取了。這裡所說的相應方法包括了所有名稱以Read開頭的方法,以及Next方法和WriteTo方法。
在寫入內容的時候,絕大多數的相應方法都會先檢查當前的內容容器,是否有足夠的容量容納新的內容。如果沒有,那麼它們就會對內容容器進行擴容。
在擴容的時候,方法會在必要時,依據已讀計數找到未讀部分,並把其中的內容拷貝到擴容後內容容器的頭部位置。
然後,方法將會把已讀計數的值置為0,以表示下一次讀取需要從內容容器的第一個位元組開始。用於寫入內容的相應方法,包括了所有名稱以Write開頭的方法,以及ReadFrom方法。
用於截斷內容的方法Truncate,會讓很多對bytes.Buffer不太了解的程式開發者迷惑。 它會接受一個int類型的參數,這個參數的值代表了:在截斷時需要保留頭部的多少個位元組。
不過,需要注意的是,這裡說的頭部指的並不是內容容器的頭部,而是其中的未讀部分的頭部。頭部的起始索引正是由已讀計數的值表示的。因此,在這種情況下,已讀計數的值再加上參數值後得到的和,就是內容容器新的總長度。
在bytes.Buffer中,用於讀回退的方法有UnreadByte和UnreadRune。 這兩個方法分別用於回退一個位元組和回退一個 Unicode 字元。調用它們一般都是為了退回在上一次被讀取內容末尾的那個分隔符,或者為重新讀取前一個位元組或字元做準備。
不過,退回的前提是,在調用它們之前的那一個操作必須是「讀取」,並且是成功的讀取,否則這些方法就只能忽略後續操作並返回一個非nil的錯誤值。
UnreadByte方法的做法比較簡單,把已讀計數的值減1就好了。而UnreadRune方法需要從已讀計數中減去的,是上一次被讀取的 Unicode 字元所佔用的位元組數。
這個位元組數由bytes.Buffer的另一個欄位負責存儲,它在這裡的有效取值範圍是[1, 4]。只有ReadRune方法才會把這個欄位的值設定在此範圍之內。
由此可見,只有緊接在調用ReadRune方法之後,對UnreadRune方法的調用才能夠成功完成。該方法明顯比UnreadByte方法的適用面更窄。
我在前面說過,bytes.Buffer的Len方法返回的是內容容器中未讀部分的長度,而不是其中已存內容的總長度(即:內容長度)。
而該類型的Bytes方法和String方法的行為,與Len方法是保持一致的。前兩個方法只會去訪問未讀部分中的內容,並返回相應的結果值。
在我們剖析了所有的相關方法之後,可以這樣來總結:在已讀計數代表的索引之前的那些內容,永遠都是已經被讀過的,它們幾乎沒有機會再次被讀取。
不過,這些已讀內容所在的記憶體空間可能會被存入新的內容。這一般都是由於重置或者擴充內容容器導致的。這時,已讀計數一定會被置為0,從而再次指向內容容器中的第一個位元組。這有時候也是為了避免記憶體分配和重用記憶體空間。
總結
總結一下,bytes.Buffer是一個集讀、寫功能於一身的數據類型。它非常適合作為位元組序列的緩衝區。我們會在下一篇文章中繼續對 bytes.Buffer 的知識進行延展。
package main
import (
"bytes"
"fmt"
)
func main() {
// 示例1。
var buffer1 bytes.Buffer
contents := "Simple byte buffer for marshaling data."
fmt.Printf("Write contents %q ...\n", contents)
buffer1.WriteString(contents)
fmt.Printf("The length of buffer: %d\n", buffer1.Len())
fmt.Printf("The capacity of buffer: %d\n", buffer1.Cap())
fmt.Println()
// 示例2。
p1 := make([]byte, 7)
n, _ := buffer1.Read(p1)
fmt.Printf("%d bytes were read. (call Read)\n", n)
fmt.Printf("The length of buffer: %d\n", buffer1.Len())
fmt.Printf("The capacity of buffer: %d\n", buffer1.Cap())
}
筆記源碼
//github.com/MingsonZheng/go-core-demo
本作品採用知識共享署名-非商業性使用-相同方式共享 4.0 國際許可協議進行許可。
歡迎轉載、使用、重新發布,但務必保留文章署名 鄭子銘 (包含鏈接: //www.cnblogs.com/MingsonZheng/ ),不得用於商業目的,基於本文修改後的作品務必以相同的許可發布。