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


39 | bytes包與字節串操作(下)

在上一篇文章中,我們分享了bytes.Buffer中已讀計數的大致功用,並圍繞着這個問題做了解析,下面我們來進行相關的知識擴展。

知識擴展

問題 1:bytes.Buffer的擴容策略是怎樣的?

Buffer值既可以被手動擴容,也可以進行自動擴容。並且,這兩種擴容方式的策略是基本一致的。所以,除非我們完全確定后續內容所需的字節數,否則讓Buffer值自動去擴容就好了。

在擴容的時候,Buffer值中相應的代碼(以下簡稱擴容代碼)會先判斷內容容器的剩余容量,是否可以滿足調用方的要求,或者是否足夠容納新的內容。

如果可以,那么擴容代碼會在當前的內容容器之上,進行長度擴充。

更具體地說,如果內容容器的容量與其長度的差,大於或等於另需的字節數,那么擴容代碼就會通過切片操作對原有的內容容器的長度進行擴充,就像下面這樣:

b.buf = b.buf[:length+need]

反之,如果內容容器的剩余容量不夠了,那么擴容代碼可能就會用新的內容容器去替代原有的內容容器,從而實現擴容。

不過,這里還有一步優化。

如果當前內容容器的容量的一半,仍然大於或等於其現有長度(即未讀字節數)再加上另需的字節數的和,即:

cap(b.buf)/2 >= b.Len() + need

那么,擴容代碼就會復用現有的內容容器,並把容器中的未讀內容拷貝到它的頭部位置。

這也意味着其中的已讀內容,將會全部被未讀內容和之后的新內容覆蓋掉。

這樣的復用預計可以至少節省掉一次后續的擴容所帶來的內存分配,以及若干字節的拷貝。

若這一步優化未能達成,也就是說,當前內容容器的容量小於新長度的二倍。

那么,擴容代碼就只能再創建一個新的內容容器,並把原有容器中的未讀內容拷貝進去,最后再用新的容器替換掉原有的容器。這個新容器的容量將會等於原有容量的二倍再加上另需字節數的和。

新容器的容量 =2* 原有容量 + 所需字節數

通過上面這些步驟,對內容容器的擴充基本上就完成了。不過,為了內部數據的一致性,以及避免原有的已讀內容可能造成的數據混亂,擴容代碼還會把已讀計數置為0,並再對內容容器做一下切片操作,以掩蓋掉原有的已讀內容。

順便說一下,對於處在零值狀態的Buffer值來說,如果第一次擴容時的另需字節數不大於64,那么該值就會基於一個預先定義好的、長度為64的字節數組來創建內容容器。

在這種情況下,這個內容容器的容量就是64。這樣做的目的是為了讓Buffer值在剛被真正使用的時候就可以快速地做好准備。

package main

import (
	"bytes"
	"fmt"
)

func main() {
	// 示例1。
	var contents string
	buffer1 := bytes.NewBufferString(contents)
	fmt.Printf("The length of new buffer with contents %q: %d\n",
		contents, buffer1.Len())
	fmt.Printf("The capacity of new buffer with contents %q: %d\n",
		contents, buffer1.Cap())
	fmt.Println()

	contents = "12345"
	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()

	contents = "67"
	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()

	contents = "89"
	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.Print("\n\n")

	// 示例2。
	contents = "abcdefghijk"
	buffer2 := bytes.NewBufferString(contents)
	fmt.Printf("The length of new buffer with contents %q: %d\n",
		contents, buffer2.Len())
	fmt.Printf("The capacity of new buffer with contents %q: %d\n",
		contents, buffer2.Cap())
	fmt.Println()

	n := 10
	fmt.Printf("Grow the buffer with %d ...\n", n)
	buffer2.Grow(n)
	fmt.Printf("The length of buffer: %d\n", buffer2.Len())
	fmt.Printf("The capacity of buffer: %d\n", buffer2.Cap())
	fmt.Print("\n\n")

	// 示例3。
	var buffer3 bytes.Buffer
	fmt.Printf("The length of new buffer: %d\n", buffer3.Len())
	fmt.Printf("The capacity of new buffer: %d\n", buffer3.Cap())
	fmt.Println()

	contents = "xyz"
	fmt.Printf("Write contents %q ...\n", contents)
	buffer3.WriteString(contents)
	fmt.Printf("The length of buffer: %d\n", buffer3.Len())
	fmt.Printf("The capacity of buffer: %d\n", buffer3.Cap())
}

問題 2:bytes.Buffer中的哪些方法可能會造成內容的泄露?

首先明確一點,什么叫內容泄露?這里所說的內容泄露是指,使用Buffer值的一方通過某種非標准的(或者說不正式的)方式,得到了本不該得到的內容。

比如說,我通過調用Buffer值的某個用於讀取內容的方法,得到了一部分未讀內容。我應該,也只應該通過這個方法的結果值,拿到在那一時刻Buffer值中的未讀內容。

但是,在這個Buffer值又有了一些新內容之后,我卻可以通過當時得到的結果值,直接獲得新的內容,而不需要再次調用相應的方法。

這就是典型的非標准讀取方式。這種讀取方式是不應該存在的,即使存在,我們也不應該使用。因為它是在無意中(或者說一不小心)暴露出來的,其行為很可能是不穩定的。

在bytes.Buffer中,Bytes方法和Next方法都可能會造成內容的泄露。原因在於,它們都把基於內容容器的切片直接返回給了方法的調用方。

我們都知道,通過切片,我們可以直接訪問和操縱它的底層數組。不論這個切片是基於某個數組得來的,還是通過對另一個切片做切片操作獲得的,都是如此。

在這里,Bytes方法和Next方法返回的字節切片,都是通過對內容容器做切片操作得到的。也就是說,它們與內容容器共用了同一個底層數組,起碼在一段時期之內是這樣的。

以Bytes方法為例。它會返回在調用那一刻其所屬值中的所有未讀內容。示例代碼如下:

contents := "ab"
buffer1 := bytes.NewBufferString(contents)
fmt.Printf("The capacity of new buffer with contents %q: %d\n",
 contents, buffer1.Cap()) // 內容容器的容量為:8。
unreadBytes := buffer1.Bytes()
fmt.Printf("The unread bytes of the buffer: %v\n", unreadBytes) // 未讀內容為:[97 98]。

我用字符串值"ab"初始化了一個Buffer值,由變量buffer1代表,並打印了當時該值的一些狀態。

你可能會有疑惑,我只在這個Buffer值中放入了一個長度為2的字符串值,但為什么該值的容量卻變為了8。

雖然這與我們當前的主題無關,但是我可以提示你一下:你可以去閱讀runtime包中一個名叫stringtoslicebyte的函數,答案就在其中。

接着說buffer1。我又向該值寫入了字符串值"cdefg",此時,其容量仍然是8。我在前面通過調用buffer1的Bytes方法得到的結果值unreadBytes,包含了在那時其中的所有未讀內容。

但是,由於這個結果值與buffer1的內容容器在此時還共用着同一個底層數組,所以,我只需通過簡單的再切片操作,就可以利用這個結果值拿到buffer1在此時的所有未讀內容。如此一來,buffer1的新內容就被泄露出來了。

buffer1.WriteString("cdefg")
fmt.Printf("The capacity of buffer: %d\n", buffer1.Cap()) // 內容容器的容量仍為:8。
unreadBytes = unreadBytes[:cap(unreadBytes)]
fmt.Printf("The unread bytes of the buffer: %v\n", unreadBytes) // 基於前面獲取到的結果值可得,未讀內容為:[97 98 99 100 101 102 103 0]。

如果我當時把unreadBytes的值傳到了外界,那么外界就可以通過該值操縱buffer1的內容了,就像下面這樣:

unreadBytes[len(unreadBytes)-2] = byte('X') // 'X'的ASCII編碼為88。
fmt.Printf("The unread bytes of the buffer: %v\n", buffer1.Bytes()) // 未讀內容變為了:[97 98 99 100 101 102 88]。

現在,你應該能夠體會到,這里的內容泄露可能造成的嚴重后果了吧?

對於Buffer值的Next方法,也存在相同的問題。不過,如果經過擴容,Buffer值的內容容器或者它的底層數組被重新設定了,那么之前的內容泄露問題就無法再進一步發展了。我在 demo80.go 文件中寫了一個比較完整的示例,你可以去看一看,並揣摩一下。

package main

import (
	"bytes"
	"fmt"
)

func main() {
	// 示例1。
	contents := "ab"
	buffer1 := bytes.NewBufferString(contents)
	fmt.Printf("The capacity of new buffer with contents %q: %d\n",
		contents, buffer1.Cap())
	fmt.Println()

	unreadBytes := buffer1.Bytes()
	fmt.Printf("The unread bytes of the buffer: %v\n", unreadBytes)
	fmt.Println()

	contents = "cdefg"
	fmt.Printf("Write contents %q ...\n", contents)
	buffer1.WriteString(contents)
	fmt.Printf("The capacity of buffer: %d\n", buffer1.Cap())
	fmt.Println()

	// 只要擴充一下之前拿到的未讀字節切片unreadBytes,
	// 就可以用它來讀取甚至修改buffer中的后續內容。
	unreadBytes = unreadBytes[:cap(unreadBytes)]
	fmt.Printf("The unread bytes of the buffer: %v\n", unreadBytes)
	fmt.Println()

	value := byte('X')
	fmt.Printf("Set a byte in the unread bytes to %v ...\n", value)
	unreadBytes[len(unreadBytes)-2] = value
	fmt.Printf("The unread bytes of the buffer: %v\n", buffer1.Bytes())
	fmt.Println()

	// 不過,在buffer的內容容器真正擴容之后就無法這么做了。
	contents = "hijklmn"
	fmt.Printf("Write contents %q ...\n", contents)
	buffer1.WriteString(contents)
	fmt.Printf("The capacity of buffer: %d\n", buffer1.Cap())
	fmt.Println()

	unreadBytes = unreadBytes[:cap(unreadBytes)]
	fmt.Printf("The unread bytes of the buffer: %v\n", unreadBytes)
	fmt.Print("\n\n")

	// 示例2。
	// Next方法返回的后續字節切片也存在相同的問題。
	contents = "12"
	buffer2 := bytes.NewBufferString(contents)
	fmt.Printf("The capacity of new buffer with contents %q: %d\n",
		contents, buffer2.Cap())
	fmt.Println()

	nextBytes := buffer2.Next(2)
	fmt.Printf("The next bytes of the buffer: %v\n", nextBytes)
	fmt.Println()

	contents = "34567"
	fmt.Printf("Write contents %q ...\n", contents)
	buffer2.WriteString(contents)
	fmt.Printf("The capacity of buffer: %d\n", buffer2.Cap())
	fmt.Println()

	// 只要擴充一下之前拿到的后續字節切片nextBytes,
	// 就可以用它來讀取甚至修改buffer中的后續內容。
	nextBytes = nextBytes[:cap(nextBytes)]
	fmt.Printf("The next bytes of the buffer: %v\n", nextBytes)
	fmt.Println()

	value = byte('X')
	fmt.Printf("Set a byte in the next bytes to %v ...\n", value)
	nextBytes[len(nextBytes)-2] = value
	fmt.Printf("The unread bytes of the buffer: %v\n", buffer2.Bytes())
	fmt.Println()

	// 不過,在buffer的內容容器真正擴容之后就無法這么做了。
	contents = "89101112"
	fmt.Printf("Write contents %q ...\n", contents)
	buffer2.WriteString(contents)
	fmt.Printf("The capacity of buffer: %d\n", buffer2.Cap())
	fmt.Println()

	nextBytes = nextBytes[:cap(nextBytes)]
	fmt.Printf("The next bytes of the buffer: %v\n", nextBytes)
}

總結

我們結合兩篇內容總結一下。與strings.Builder類型不同,bytes.Buffer不但可以拼接、截斷其中的字節序列,以各種形式導出其中的內容,還可以順序地讀取其中的子序列。

bytes.Buffer類型使用字節切片作為其內容容器,並且會用一個字段實時地記錄已讀字節的計數。

雖然我們無法直接計算出這個已讀計數,但是由於它在Buffer值中起到的作用非常關鍵,所以我們很有必要去理解它。

無論是讀取、寫入、截斷、導出還是重置,已讀計數都是功能實現中的重要一環。

與strings.Builder類型的值一樣,Buffer值既可以被手動擴容,也可以進行自動的擴容。除非我們完全確定后續內容所需的字節數,否則讓Buffer值自動去擴容就好了。

Buffer值的擴容方法並不一定會為了獲得更大的容量,替換掉現有的內容容器,而是先會本着盡量減少內存分配和內容拷貝的原則,對當前的內容容器進行重用。並且,只有在容量實在無法滿足要求的時候,它才會去創建新的內容容器。

此外,你可能並沒有想到,Buffer值的某些方法可能會造成內容的泄露。這主要是由於這些方法返回的結果值,在一段時期內會與其所屬值的內容容器共用同一個底層數組。

如果我們有意或無意地把這些結果值傳到了外界,那么外界就有可能通過它們操縱相關聯Buffer值的內容。

這屬於很嚴重的數據安全問題。我們一定要避免這種情況的發生。最徹底的做法是,在傳出切片這類值之前要做好隔離。比如,先對它們進行深度拷貝,然后再把副本傳出去。

思考題

今天的思考題是:對比strings.Builder和bytes.Buffer的String方法,並判斷哪一個更高效?原因是什么?

筆記源碼

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

知識共享許可協議

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

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM