溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

Go36-38,39-bytes包

發(fā)布時間:2020-07-02 12:36:03 來源:網(wǎng)絡 閱讀:416 作者:騎士救兵 欄目:編程語言

基本操作

bytes包和strings包非常相似,單從它們提供的函數(shù)的數(shù)量和功能上看,差別微乎其微。
strings包主要是面向Unicode字符和經(jīng)過UTF-8編碼的字符串,而bytes包主要是面對字節(jié)和字節(jié)切片。

bytes.Buffer類型

Buffer類型的用途主要是作為字節(jié)序列的緩沖區(qū)。
bytes.Buffer是開箱即用的??梢赃M行拼接、截斷其中的字節(jié)序列,以各種形式導出其中的內(nèi)容,還可以順序的讀取其中的子序列。所以是集讀、寫功能與一身的數(shù)據(jù)類型,這些也是作為緩沖區(qū)應該擁有的功能。
在內(nèi)部,bytes.Buffer類型使用字節(jié)切片(bootstrap字段)作為內(nèi)容容器。還有一個int類型(off字段)作為已讀字節(jié)的計數(shù)器,簡稱為已讀計數(shù)。不過這里的已讀計數(shù)是不無獲取也無法計算得到的。bytes.Buffer類型具體如下:

type Buffer struct {
    buf       []byte   // contents are the bytes buf[off : len(buf)]
    off       int      // read at &buf[off], write at &buf[len(buf)]
    bootstrap [64]byte // memory to hold first slice; helps small buffers avoid allocation.
    lastRead  readOp   // 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())
}

/* 執(zhí)行結(jié)果
PS G:\Steed\Documents\Go\src\Go36\article38\example01> go run main.go
Lan: 14 Cap: 64
5 Make
Lan: 9  Cap: 64
PS G:\Steed\Documents\Go\src\Go36\article38\example01>
*/

先聲明了一個byte.Buffer類型的變量,并寫入一個字符串。然后打印了這個Buffer值的長度和容量。之后進行了一次讀取,讀取之后,再輸出一個長度和容量。這里容量沒有變,因為沒有再寫入任何內(nèi)容。而長度變小了,這里的長度是未讀內(nèi)容的長度,一開始和存放的字節(jié)序列的長度一樣,在讀取操作之后,會隨之變小,同樣的,在寫入操作之后,也會增大。

已讀計數(shù)

沒有辦法可以直接得到Buffer值的已讀計數(shù),并且也很難估算它。但是為了用好bytes.Buffer,依然需要去源碼里了解一下已讀計數(shù)的作用。
bytes.Buffer中的已讀計數(shù)的大致的功用如下:

  1. 讀取內(nèi)容時,相應方法會依據(jù)已讀計數(shù)找到未讀部分,并在讀取后更新計數(shù)
  2. 寫入內(nèi)容時,如需擴容,相應方法會根據(jù)已讀計數(shù)實現(xiàn)擴容策略
  3. 截斷內(nèi)容時,相應方法截掉的是已讀計數(shù)代表的索引之后的未讀部分
  4. 讀回退時,相應方法需要用已讀計數(shù)記錄回退點
  5. 重置內(nèi)容時,相應方法會把已讀計數(shù)置為0
  6. 導出內(nèi)容時,相應方法會導出已讀計數(shù)代表的索引之后的未讀部分
  7. 獲取長度時,相應方法會依據(jù)已讀計數(shù)和內(nèi)容容器的長度,計算未讀部分的長度并返回

通過以上功能的介紹,就能夠體會到已讀計數(shù)的重要性了。在bytes.Buffer的大多數(shù)的方法都用到了已讀計數(shù),而且都是非用不可的。

讀取內(nèi)容

在讀取內(nèi)容的時候,相應方法會先根據(jù)已讀計數(shù),判斷一下內(nèi)容容器中是否還有未讀內(nèi)容。如果有,那就會以已讀計數(shù)為索引開始讀取。讀完之后,還會及時的更新已讀計數(shù)。
讀取內(nèi)容的方法:

  • 所有名稱以Read開頭的方法
  • Next方法
  • WriteTo方法

寫入內(nèi)容

在寫入內(nèi)容的時候,絕大多數(shù)的響應方法都會先檢查當前的內(nèi)容容器,看看是否有足夠的容量容納新內(nèi)容。如果沒有,就會進行擴容。在擴容的時候,方法會在必要時,依據(jù)已讀計數(shù)找到未讀部分,并把其中的內(nèi)容拷貝到擴容后的內(nèi)容容器的頭部位置。然后,方法將會把已讀計數(shù)的值置為0,這樣下一次讀取的時候就會從新的內(nèi)容容器的第一個字節(jié)開始了。
由于擴容后,已讀的內(nèi)容不會拷貝,所以就真正的丟棄了。不過Buffer本身也不支持對已讀內(nèi)容的再次操作,只是出于效率和值不可變的考慮,不會進行刪除,而是等到擴容的時候忽略該部分內(nèi)容不做拷貝,最后等著被回收掉。
寫入內(nèi)容的方法:

  • 所有名稱以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())
}

截斷內(nèi)容

截斷內(nèi)容的方法:Truncate
該方法會接受一個int類型的參數(shù),表示在截斷時需要保留頭部的多個個字節(jié)。注意這里所說的頭部指的是未讀部分的頭部。這個頭部的起始索引正是已讀計數(shù)的值。
還是因為已讀部分邏輯上就是不存在的,所以這里截斷操作是從未讀部分開始的

讀回退

讀回退有2個方法:

  • UnreadByte : 回退一個一節(jié)
  • UnreadRune : 回退一個Unicode字符

調(diào)用它們一般是為了退回到上一次被讀取內(nèi)容末尾的那個分隔符,或者為了重新讀取前一個字節(jié)或字符做準備。回退是有前提的,在調(diào)用之前的哪一個操作必須是讀取內(nèi)容,并且是成功讀取的。否則這寫方法就會忽略后續(xù)操作并返回一個非nil的錯誤值。
UnreadByte方法比較簡單,直接已讀計數(shù)減1即可。
而UnreadRune方法需要從已讀計數(shù)中減去的,是上一次被讀取的Unicode字符所占用的字節(jié)數(shù)。這個字節(jié)數(shù)存在bytes.Buffer的lastRead字段里。只有在執(zhí)行ReadRune方法中才會把這個字段設置為1至4的值,其他一些讀寫的方法中會在這個字段設置為0或-1。所以只有緊接在ReadRune方法之后,才能成功調(diào)用UnreadRune方法。這個方法明顯比UnreadByte方法的適用面更小。

重置內(nèi)容

重置內(nèi)容的方法:Reset
不多解釋了,直接看源碼:

func (b *Buffer) Reset() {
    b.buf = b.buf[:0]
    b.off = 0
    b.lastRead = opInvalid
}

沒有重置內(nèi)容容器,這樣避免了一次內(nèi)存分配。

導出內(nèi)容

導出內(nèi)容的方法:

  • Bytes方法
  • String方法

訪問未讀部分的中的內(nèi)容,并返回相應的結(jié)果。已讀的部分可以認為是邏輯丟棄了,如果有過擴容,在垃圾清理后就是真正的物理丟棄了,所以也不應該獲取到。

獲取長度

獲取長度的方法:Lan方法
返回內(nèi)容容器中未讀部分的長度。而不是其中已存內(nèi)容的總長度,即:內(nèi)容長度。

小結(jié)

已讀計數(shù)器索引之前的那些內(nèi)容,永遠都是已經(jīng)被讀過的,幾乎沒有機會再次被讀取到。
不過,這些已讀內(nèi)容所在的內(nèi)存空間可能會被存入新的內(nèi)容。這一般都是由于重置或者擴容內(nèi)容容器導致的。重置或擴容后,已讀計數(shù)一定會被置0,從而再次指向內(nèi)容容器中的第一個字節(jié)。這有時候也是為了避免內(nèi)存分配和重用內(nèi)存空間,這句意思大概是:重用一次內(nèi)容空間的話,就避免了一次內(nèi)存分配的操作。直接把之前分配過的但是內(nèi)容已經(jīng)不需要的內(nèi)存再用起來。否則的話,就是一次新的內(nèi)存分配和一次對已分配內(nèi)存的清理

擴展知識

主要講兩個問題:

  • 擴容策略
  • 內(nèi)容泄露

擴容策略

Buffer值既可以被手動擴容,也可以進行自動擴容。并且這種擴容方式的策略是基本一致的。所以,在完全確定后續(xù)內(nèi)容所需的字節(jié)數(shù)的時候手動擴容,否則讓Buffer值自動擴容就好了。
在擴容的時候,是會先判斷內(nèi)容容器(bootstrap)的剩余容量是否夠用,如果可以,會在當前的內(nèi)容容器上,進行長度擴容。在源碼中就是下面這幾句體現(xiàn)的:

func (b *Buffer) grow(n int) int {
    m := b.Len()
    // 省略中間的若干代碼
    b.buf = b.buf[:m+n]  // 當前內(nèi)容的長度+需要的長度
    return m
}

若干內(nèi)容容器的剩余容量不夠了,那么擴容就會用新的內(nèi)容容器去替代原有的內(nèi)容容器,從而實現(xiàn)擴容。這里會有一步優(yōu)化,如果當前內(nèi)容容器的容量的一半仍然大于或等于現(xiàn)有長度在加上需要的字節(jié)數(shù),那么擴容代碼會復用現(xiàn)有的內(nèi)容容器,并把容器中未讀內(nèi)容拷貝到它的頭部位置。這樣就是把已讀內(nèi)容都覆蓋掉了,整體內(nèi)容在內(nèi)存里往前移。這樣的復用可以省掉一次后續(xù)的擴容所帶來的內(nèi)存分配,以及若干字節(jié)的拷貝。
若上面的優(yōu)化條件不滿足,那么擴容代碼就要再創(chuàng)建一個新的內(nèi)容容器,并把原有容器中的未讀內(nèi)容拷貝進去,最后再用新的容器替換掉原有的容器。這個新容器的容量講會等于原有容量的兩倍再加上需要的字節(jié)數(shù)。這個策略和之前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
}

如果對處于零值狀態(tài)的Buffer值來說,如果第一次擴容時需要的字節(jié)數(shù)不大于64,那么該值就會基于一個預先定義好的、長度為64的數(shù)組([64]byte)來作為內(nèi)容容器。這樣做的目的是為了讓Buffer值在剛被真正使用的時候就可以快速的做好準備。
完成上面的步驟,對內(nèi)容容器的擴容就基本完成了。不過,為了內(nèi)部數(shù)據(jù)的一致性,以及避免原有的已讀內(nèi)容可能造成的數(shù)據(jù)混亂,擴容代碼還會把已讀計數(shù)置為0,并再對內(nèi)容容器做一下切片操作,以掩蓋掉原有的已讀內(nèi)容。

注意內(nèi)容泄露

內(nèi)容泄露:這里說的內(nèi)容泄露是指,使用Buffer值的一個方法通過某種非標準的(或者說不正式的)方法得到了不該得到的內(nèi)容。
比如,通過調(diào)用Buffer值的某個用于讀取內(nèi)容的方法,得到了一部分未讀內(nèi)容。但是這個Buffer值又有了一些新內(nèi)容后,卻可以通過當時得到的結(jié)果值,直接獲得新的內(nèi)容,而不需要再次調(diào)用相應的讀去內(nèi)容的方法。這就是典型的非標準讀取方式。這種讀取方式是不應該存在的,即使存在,也不應該使用。因為它是在無意中(或者說不小心)暴露出來的,其行為很可能是不穩(wěn)定的。
在bytes.Buffer中,Bytes方法和Next方法都可能會造成內(nèi)容的泄露。原因在于,它們都把基于內(nèi)容容器的切片直接返回給了方法的調(diào)用方。通過切片,就可以直接訪問和操作它的底層數(shù)組。不論這個切片是基于某個數(shù)組得來的,還是通過對另一個切片做切片操作獲得的。這里的Bytes方法和Next方法返回的字節(jié)切片,都是通過對內(nèi)容容器做切片操作得到的。也就是說,它們與內(nèi)容容器公用了同一個底層數(shù)組,起碼在一段時期之內(nèi)是這樣的。
以Bytes方法為例,下面是演示內(nèi)容泄露的示例:

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)
    // 這里只要擴充一下切片,就讀到后續(xù)內(nèi)容了
    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)
}

這里要避免擴容,寫入內(nèi)容后都輸出了一下容量,容量不變就是沒有擴容過。那么Bytes方法返回的結(jié)果值與內(nèi)容容器在此時還共用著同一個底層數(shù)組。之后就簡單的做了再切片,就通過這個結(jié)果值把后面的未讀內(nèi)容都拿到了。這還沒完,如果當時把這個值傳到了外界,那么外界就可以通過該值修改里面的內(nèi)容了。這個后果就很嚴重了,另一個Next方法,也存在相同的問題。
不過,如果經(jīng)過擴容,Buffer值的內(nèi)容容器或者它的底層數(shù)組就被重新設定了,那么之前的內(nèi)容泄露問題就無法再進一步發(fā)展了。
這里是一個很嚴重的數(shù)據(jù)安全問題。一定要避免這種情況的發(fā)生。泄露的包里的方法本身的特性,無法避免,但是可以小心操作。會造成嚴重后果的途徑是有意或無意的把這些返回的結(jié)果值傳到了外界,這個問題可以避免。要在傳出切片這類值之前,做好隔離。不如,先對它們進行深拷貝,然后再把副本傳出去。

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI