Go36-38,39-bytes包
基本操作
bytes包和strings包非常相似,單從它們提供的函式的數量和功能上看,差別微乎其微。
strings包主要是面向Unicode字元和經過UTF-8編碼的字串,而bytes包主要是面對位元組和位元組切片。
bytes.Buffer型別
Buffer型別的用途主要是作為位元組序列的緩衝區。
bytes.Buffer是開箱即用的。可以進行拼接、截斷其中的位元組序列,以各種形式匯出其中的內容,還可以順序的讀取其中的子序列。所以是集讀、寫功能與一身的資料型別,這些也是作為緩衝區應該擁有的功能。
在內部,bytes.Buffer型別使用位元組切片(bootstrap欄位)作為內容容器 。還有一個int型別(off欄位)作為已讀位元組的計數器,簡稱為已讀計數 。不過這裡的已讀計數是不無獲取也無法計算得到的。bytes.Buffer型別具體如下:
type Buffer struct { buf[]byte// contents are the bytes buf[off : len(buf)] offint// read at &buf[off], write at &buf[len(buf)] bootstrap [64]byte // memory to hold first slice; helps small buffers avoid allocation. lastReadreadOp// last read operation, so that Unread* can work correctly. }
長度和容量
先看下示例:
package main import ( "fmt" "bytes" ) func main() { var b1 bytes.Buffer contents := "Make the plan." b1.WriteString(contents) fmt.Println(b1.Len(), b1.Cap()) p1 := make([]byte, 5) n, _ := b1.Read(p1)// 忽略錯誤 fmt.Println(n, string(p1)) fmt.Println(b1.Len(), b1.Cap()) } /* 執行結果 PS G:\Steed\Documents\Go\src\Go36\article38\example01> go run main.go Lan: 14 Cap: 64 5 Make Lan: 9Cap: 64 PS G:\Steed\Documents\Go\src\Go36\article38\example01> */
先聲明瞭一個byte.Buffer型別的變數,並寫入一個字串。然後列印了這個Buffer值的長度和容量。之後進行了一次讀取,讀取之後,再輸出一個長度和容量。這裡容量沒有變,因為沒有再寫入任何內容。而長度變小了,這裡的長度是未讀內容的長度,一開始和存放的位元組序列的長度一樣,在讀取操作之後,會隨之變小,同樣的,在寫入操作之後,也會增大。
已讀計數
沒有辦法可以直接得到Buffer值的已讀計數,並且也很難估算它。但是為了用好bytes.Buffer,依然需要去原始碼裡瞭解一下已讀計數的作用。
bytes.Buffer中的已讀計數的大致的功用如下:
- 讀取內容時,相應方法會依據已讀計數找到未讀部分,並在讀取後更新計數
- 寫入內容時,如需擴容,相應方法會根據已讀計數實現擴容策略
- 截斷內容時,相應方法截掉的是已讀計數代表的索引之後的未讀部分
- 讀回退時,相應方法需要用已讀計數記錄回退點
- 重置內容時,相應方法會把已讀計數置為0
- 匯出內容時,相應方法會匯出已讀計數代表的索引之後的未讀部分
- 獲取長度時,相應方法會依據已讀計數和內容容器的長度,計算未讀部分的長度並返回
通過以上功能的介紹,就能夠體會到已讀計數的重要性了。在bytes.Buffer的大多數的方法都用到了已讀計數,而且都是非用不可的。
讀取內容
在讀取內容的時候,相應方法會先根據已讀計數,判斷一下內容容器中是否還有未讀內容。如果有,那就會以已讀計數為索引開始讀取。讀完之後,還會及時的更新已讀計數。
讀取內容的方法:
- 所有名稱以Read開頭的方法
- Next方法
- WriteTo方法
寫入內容
在寫入內容的時候,絕大多數的響應方法都會先檢查當前的內容容器,看看是否有足夠的容量容納新內容。如果沒有,就會進行擴容。在擴容的時候,方法會在必要時,依據已讀計數找到未讀部分,並把其中的內容拷貝到擴容後的內容容器的頭部位置。然後,方法將會把已讀計數的值置為0,這樣下一次讀取的時候就會從新的內容容器的第一個位元組開始了。
由於擴容後,已讀的內容不會拷貝,所以就真正的丟棄了。不過Buffer本身也不支援對已讀內容的再次操作,只是出於效率和值不可變的考慮,不會進行刪除,而是等到擴容的時候忽略該部分內容不做拷貝,最後等著被回收掉。 寫入內容的方法:
- 所有名稱以Write開頭的方法
- ReadFrom方法
示例:
func main() { var contents string b1 := bytes.NewBufferString(contents) fmt.Printf("Lan: %d, Cap: %d.\n", b1.Len(), b1.Cap()) contents = "一二三四五" b1.WriteString(contents) fmt.Printf("Lan: %d, Cap: %d.\n", b1.Len(), b1.Cap()) contents = "67" b1.WriteString(contents) fmt.Printf("Lan: %d, Cap: %d.\n", b1.Len(), b1.Cap()) }
截斷內容
截斷內容的方法:Truncate
該方法會接受一個int型別的引數,表示在截斷時需要保留頭部的多個個位元組。注意這裡所說的頭部指的是未讀部分的頭部。這個頭部的起始索引正是已讀計數的值。
還是因為已讀部分邏輯上就是不存在的,所以這裡截斷操作是從未讀部分開始的 。
讀回退
讀回退有2個方法:
- UnreadByte : 回退一個一節
- UnreadRune : 回退一個Unicode字元
呼叫它們一般是為了退回到上一次被讀取內容末尾的那個分隔符,或者為了重新讀取前一個位元組或字元做準備。回退是有前提的,在呼叫之前的哪一個操作必須是讀取內容,並且是成功讀取的。否則這寫方法就會忽略後續操作並返回一個非nil的錯誤值。
UnreadByte方法比較簡單,直接已讀計數減1即可。
而UnreadRune方法需要從已讀計數中減去的,是上一次被讀取的Unicode字元所佔用的位元組數。這個位元組數存在bytes.Buffer的lastRead欄位裡。只有在執行ReadRune方法中才會把這個欄位設定為1至4的值,其他一些讀寫的方法中會在這個欄位設定為0或-1。所以只有緊接在ReadRune方法之後,才能成功呼叫UnreadRune方法。這個方法明顯比UnreadByte方法的適用面更小。
重置內容
重置內容的方法:Reset
不多解釋了,直接看原始碼:
func (b *Buffer) Reset() { b.buf = b.buf[:0] b.off = 0 b.lastRead = opInvalid }
沒有重置內容容器,這樣避免了一次記憶體分配。
匯出內容
匯出內容的方法:
- Bytes方法
- String方法
訪問未讀部分的中的內容,並返回相應的結果。已讀的部分可以認為是邏輯丟棄了,如果有過擴容,在垃圾清理後就是真正的物理丟棄了,所以也不應該獲取到。
獲取長度
獲取長度的方法:Lan方法
返回內容容器中未讀部分的長度。而不是其中已存內容的總長度,即:內容長度。
小結
已讀計數器索引之前的那些內容,永遠都是已經被讀過的,幾乎沒有機會再次被讀取到。
不過,這些已讀內容所在的記憶體空間可能會被存入新的內容。這一般都是由於重置或者擴容內容容器導致的。重置或擴容後,已讀計數一定會被置0,從而再次指向內容容器中的第一個位元組。這有時候也是為了避免記憶體分配和重用記憶體空間,這句意思大概是:重用一次內容空間的話,就避免了一次記憶體分配的操作。直接把之前分配過的但是內容已經不需要的記憶體再用起來。否則的話,就是一次新的記憶體分配和一次對已分配記憶體的清理 。
擴充套件知識
主要講兩個問題:
- 擴容策略
- 內容洩露
擴容策略
Buffer值既可以被手動擴容,也可以進行自動擴容。並且這種擴容方式的策略是基本一致的。所以,在完全確定後續內容所需的位元組數的時候手動擴容,否則讓Buffer值自動擴容就好了。
在擴容的時候,是會先判斷內容容器(bootstrap)的剩餘容量是否夠用,如果可以,會在當前的內容容器上,進行長度擴容。在原始碼中就是下面這幾句體現的:
func (b *Buffer) grow(n int) int { m := b.Len() // 省略中間的若干程式碼 b.buf = b.buf[:m+n]// 當前內容的長度+需要的長度 return m }
若干內容容器的剩餘容量不夠了,那麼擴容就會用新的內容容器去替代原有的內容容器,從而實現擴容。這裡會有一步優化,如果當前內容容器的容量的一半仍然大於或等於現有長度在加上需要的位元組數,那麼擴容程式碼會複用現有的內容容器,並把容器中未讀內容拷貝到它的頭部位置。這樣就是把已讀內容都覆蓋掉了,整體內容在記憶體裡往前移。這樣的複用可以省掉一次後續的擴容所帶來的記憶體分配,以及若干位元組的拷貝。
若上面的優化條件不滿足,那麼擴容程式碼就要再建立一個新的內容容器,並把原有容器中的未讀內容拷貝進去,最後再用新的容器替換掉原有的容器。這個新容器的容量講會等於原有容量的兩倍再加上需要的位元組數。這個策略和之前strings擴容的策略是一樣的 。
下面是一個擴容的示例程式碼:
func main() { contents := "Good Year!" b1 := bytes.NewBufferString(contents) fmt.Printf("Lan: %d, Cap: %d.\n", b1.Len(), b1.Cap())// 10, 16 n := 10 b1.Grow(n) fmt.Printf("Lan: %d, Cap: %d.\n", b1.Len(), b1.Cap())// 10, 42 }
如果對處於零值狀態的Buffer值來說,如果第一次擴容時需要的位元組數不大於64,那麼該值就會基於一個預先定義好的、長度為64的陣列([64]byte)來作為內容容器。這樣做的目的是為了讓Buffer值在剛被真正使用的時候就可以快速的做好準備。
完成上面的步驟,對內容容器的擴容就基本完成了。不過,為了內部資料的一致性,以及避免原有的已讀內容可能造成的資料混亂,擴容程式碼還會把已讀計數置為0,並再對內容容器做一下切片操作,以掩蓋掉原有的已讀內容。
注意內容洩露
內容洩露:這裡說的內容洩露是指,使用Buffer值的一個方法通過某種非標準的(或者說不正式的)方法得到了不該得到的內容。
比如,通過呼叫Buffer值的某個用於讀取內容的方法,得到了一部分未讀內容。但是這個Buffer值又有了一些新內容後,卻可以通過當時得到的結果值,直接獲得新的內容,而不需要再次呼叫相應的讀去內容的方法。這就是典型的非標準讀取方式。這種讀取方式是不應該存在的,即使存在,也不應該使用。因為它是在無意中(或者說不小心)暴露出來的,其行為很可能是不穩定的。
在bytes.Buffer中,Bytes方法和Next方法都可能會造成內容的洩露。原因在於,它們都把基於內容容器的切片直接返回給了方法的呼叫方。通過切片,就可以直接訪問和操作它的底層陣列。不論這個切片是基於某個陣列得來的,還是通過對另一個切片做切片操作獲得的。這裡的Bytes方法和Next方法返回的位元組切片,都是通過對內容容器做切片操作得到的。也就是說,它們與內容容器公用了同一個底層陣列,起碼在一段時期之內是這樣的。
以Bytes方法為例,下面是演示內容洩露的示例:
func main() { b1 := bytes.NewBufferString("abc") fmt.Printf("Lan: %d, Cap: %d.\n", b1.Len(), b1.Cap()) s1 := b1.Bytes() fmt.Printf("%[1]v, %[1]s\n", s1) b1.WriteString("123") fmt.Printf("Lan: %d, Cap: %d.\n", b1.Len(), b1.Cap()) fmt.Printf("%[1]v, %[1]s\n", s1) // 這裡只要擴充一下切片,就讀到後續內容了 s1 = s1[:cap(s1)] fmt.Printf("%[1]v, %[1]s\n", s1) // 只是讀到還不算,還能改 s1[len(s1)-3] = 'X' fmt.Printf("%[1]v, %[1]s\n", s1) }
這裡要避免擴容,寫入內容後都輸出了一下容量,容量不變就是沒有擴容過。那麼Bytes方法返回的結果值與內容容器在此時還共用著同一個底層陣列。之後就簡單的做了再切片,就通過這個結果值把後面的未讀內容都拿到了。這還沒完,如果當時把這個值傳到了外界,那麼外界就可以通過該值修改裡面的內容了。這個後果就很嚴重了,另一個Next方法,也存在相同的問題。
不過,如果經過擴容,Buffer值的內容容器或者它的底層陣列就被重新設定了,那麼之前的內容洩露問題就無法再進一步發展了。
這裡是一個很嚴重的資料安全問題。一定要避免這種情況的發生。洩露的包裡的方法本身的特性,無法避免,但是可以小心操作。會造成嚴重後果的途徑是有意或無意的把這些返回的結果值傳到了外界,這個問題可以避免。要在傳出切片這類值之前,做好隔離。不如,先對它們進行深拷貝,然後再把副本傳出去。