Go語言核心36講(Go語言實戰與應用十六)--學習筆記

MingsonZheng發表於2021-11-28

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())
}

筆記原始碼

https://github.com/MingsonZheng/go-core-demo

知識共享許可協議

本作品採用知識共享署名-非商業性使用-相同方式共享 4.0 國際許可協議進行許可。

歡迎轉載、使用、重新發布,但務必保留文章署名 鄭子銘 (包含連結: http://www.cnblogs.com/MingsonZheng/ ),不得用於商業目的,基於本文修改後的作品務必以相同的許可釋出。

相關文章