首頁 >後端開發 >Golang >一文了解golang slice和string的重複使用

一文了解golang slice和string的重複使用

藏色散人
藏色散人轉載
2021-07-16 15:34:282447瀏覽

相比於c/c ,golang 的一個很大的改進就是引入了gc 機制,不再需要使用者自己管理內存,大大減少了程式由於記憶體外洩而引入的bug,但是同時gc 也帶來了額外的效能開銷,有時甚至會因為使用不當,導致gc 成為效能瓶頸,所以golang 程式設計的時候,應特別注意物件的重用,以減少gc 的壓力。而slice 和string 是golang 的基本類型,了解這些基本類型的內部機制,有助於我們更好地重複使用這些物件

slice 和string 內部結構

slice 和string 的內部結構可以在$GOROOT/src/reflect/value.go 裡面找到

type StringHeader struct {
    Data uintptr
    Len  int
}

type SliceHeader struct {
    Data uintptr
    Len  int
    Cap  int
}

可以看到一個string 包含一個資料指標和一個長度,長度是不可變的

slice 包含一個資料指標、一個長度和一個容量,當容量不夠時會重新申請新的內存,Data 指標將指向新的地址,原來的地址空間將被釋放

從這些結構就可以看出,string 和slice 的賦值,包括當做參數傳遞,和自訂的結構體一樣,都只是Data 指標的淺拷貝

slice 重用

append 運算

si1 := []int{1, 2, 3, 4, 5, 6, 7, 8, 9}
si2 := si1
si2 = append(si2, 0)
Convey("重新分配内存", func() {
    header1 := (*reflect.SliceHeader)(unsafe.Pointer(&si1))
    header2 := (*reflect.SliceHeader)(unsafe.Pointer(&si2))
    fmt.Println(header1.Data)
    fmt.Println(header2.Data)
    So(header1.Data, ShouldNotEqual, header2.Data)
})

si1 和si2 開始都指向同一個數組,當對si2 執行append 操作時,由於原來的Cap 值不夠了,需要重新申請新的空間,因此Data 值發生了變化,在$GOROOT /src/reflect/value.go 這個檔案裡面還有關於新的cap 值的策略,在grow 這個函數裡面,當cap 小於1024 的時候,是成倍的增長,超過的時候,每次增長25%,而這種內存增長不僅僅數據拷貝(從舊的地址拷貝到新的地址)需要消耗額外的性能,舊地址內存的釋放對gc 也會造成額外的負擔,所以如果能夠知道資料的長度的情況下,盡量使用make([]int, len, cap) 預先分配內存,不知道長度的情況下,可以考慮下面的記憶體重用的方法

記憶體重複使用

si1 := []int{1, 2, 3, 4, 5, 6, 7, 8, 9}
si2 := si1[:7]
Convey("不重新分配内存", func() {
    header1 := (*reflect.SliceHeader)(unsafe.Pointer(&si1))
    header2 := (*reflect.SliceHeader)(unsafe.Pointer(&si2))
    fmt.Println(header1.Data)
    fmt.Println(header2.Data)
    So(header1.Data, ShouldEqual, header2.Data)
})

Convey("往切片里面 append 一个值", func() {
    si2 = append(si2, 10)
    Convey("改变了原 slice 的值", func() {
        header1 := (*reflect.SliceHeader)(unsafe.Pointer(&si1))
        header2 := (*reflect.SliceHeader)(unsafe.Pointer(&si2))
        fmt.Println(header1.Data)
        fmt.Println(header2.Data)
        So(header1.Data, ShouldEqual, header2.Data)
        So(si1[7], ShouldEqual, 10)
    })
})

si2 是si1 的一個切片,從第一段程式碼可以看到切片並沒有重新分配內存,si2 和si1 的Data 指標指向同一片位址,而第二段程式碼可以看出,當我們往si2 裡面append 一個新的值的時候,我們發現仍然沒有記憶體分配,而且這個操作使得si1 的值也發生了改變,因為兩者本來就是指向同一片Data 區域,利用這個特性,我們只需要讓si1 = si1[:0] 就可以不斷地清空si1 的內容,實現記憶體的複用了

PS: 你可以使用copy(si2, si1) 實作深拷貝

string

Convey("字符串常量", func() {
    str1 := "hello world"
    str2 := "hello world"
    Convey("地址相同", func() {
        header1 := (*reflect.StringHeader)(unsafe.Pointer(&str1))
        header2 := (*reflect.StringHeader)(unsafe.Pointer(&str2))
        fmt.Println(header1.Data)
        fmt.Println(header2.Data)
        So(header1.Data, ShouldEqual, header2.Data)
    })
})

這個範例比較簡單,字串常數使用的是同一片位址區域

Convey("相同字符串的不同子串", func() {
    str1 := "hello world"[:6]
    str2 := "hello world"[:5]
    Convey("地址相同", func() {
        header1 := (*reflect.StringHeader)(unsafe.Pointer(&str1))
        header2 := (*reflect.StringHeader)(unsafe.Pointer(&str2))
        fmt.Println(header1.Data, str1)
        fmt.Println(header2.Data, str2)
        So(str1, ShouldNotEqual, str2)
        So(header1.Data, ShouldEqual, header2.Data)
    })
})

相同字串的不同子字串,不會額外申請新的內存,但是要注意的是這裡的相同字符串,指的是str1.Data == str2.Data && str1.Len == str2. Len,而不是str1 == str2,下面這個例子可以說明str1 == str2 但是其Data 並不相同

Convey("不同字符串的相同子串", func() {
    str1 := "hello world"[:5]
    str2 := "hello golang"[:5]
    Convey("地址不同", func() {
        header1 := (*reflect.StringHeader)(unsafe.Pointer(&str1))
        header2 := (*reflect.StringHeader)(unsafe.Pointer(&str2))
        fmt.Println(header1.Data, str1)
        fmt.Println(header2.Data, str2)
        So(str1, ShouldEqual, str2)
        So(header1.Data, ShouldNotEqual, header2.Data)
    })
})

實際上對於字符串,你只需要記住一點,字符串是不可變的,任何字符串的操作都不會申請額外的內存(對於僅內部數據指針而言),我曾自作聰明地設計了一個cache 去存儲字符串,以減少重複字串所佔用的空間,事實上,除非這個字串本身就是由[]byte 創建而來,否則,這個字串本身就是另一個字串的子字串(例如透過strings.Split 得到的字串),本來就不會申請額外的空間,這麼做簡直就是多此一舉。

更多golang相關技術文章,請造訪golang教學欄位!

以上是一文了解golang slice和string的重複使用的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:segmentfault.com。如有侵權,請聯絡admin@php.cn刪除