相比於 c/c++,golang 的一個很大的改進就是引入了 gc 機制,再也不須要用戶本身管理內存,大大減小了程序因爲內存泄露而引入的 bug,可是同時 gc 也帶來了額外的性能開銷,有時甚至會由於使用不當,致使 gc 成爲性能瓶頸,因此 golang 程序設計的時候,應特別注意對象的重用,以減小 gc 的壓力。而 slice 和 string 是 golang 的基本類型,瞭解這些基本類型的內部機制,有助於咱們更好地重用這些對象c++
slice 和 string 的內部結構能夠在 $GOROOT/src/reflect/value.go
裏面找到git
type StringHeader struct { Data uintptr Len int } type SliceHeader struct { Data uintptr Len int Cap int }
能夠看到一個 string 包含一個數據指針和一個長度,長度是不可變的github
slice 包含一個數據指針、一個長度和一個容量,當容量不夠時會從新申請新的內存,Data 指針將指向新的地址,原來的地址空間將被釋放golang
從這些結構就能夠看出,string 和 slice 的賦值,包括當作參數傳遞,和自定義的結構體同樣,都僅僅是 Data 指針的淺拷貝數組
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)
預分配內存,不知道長度的狀況下,能夠考慮下面的內存重用的方法app
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)
實現深拷貝性能
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 並不相同ui
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
得到的字符串),原本就不會申請額外的空間,這麼作簡直就是畫蛇添足
轉載請註明出處
本文連接:http://hatlonely.com/2018/03/17/golang-slice-%E5%92%8C-string-%E9%87%8D%E7%94%A8/