Golang Slice 的一些事
女主宣言
使用 Golang 編程時,常會使用到一個數據結構 —— Slice,這篇文帶大家看看 Slice 具體的數據結構以及常用手法。
PS:豐富的一線技術、多元化的表現形式,盡在“HULK一線技術雜談”,點關注哦!
1. Slice 數據結構
首先,直接從源碼$YOUR_GO_DIR/src/runtime/slice.go
(其中$YOUR_GO_DIR
指你自己go源代碼的根目錄)中找到定義的slice
結構,如下:
type slice struct {
// 任意類型指針(類似C語言中的 void* ), 指向實際存儲slice數據的數組
array unsafe.Pointer
len int // length, 長度
cap int // capacity, 容量
}
從結構很好看出,通過make()函數(比如:make([]int, 10, 20))創建出來的slice,其實就是由兩部分組成:slice的"描述"(上面的結構體)
+ 存儲數據的數組(指針指向的數組)
。
備注:后文將使用SliceHeader代替上面的結構體,SliceHeader是Rob Pike在Golang/slices博文中暫用來指代的名詞,這里我也借用一下。為了方便理解,把slice拆成 SliceHeader + 存儲數據的數組 兩部分。
2. 使用 Slice 須知
2.1 值傳遞下的Slice
我們知道Go的參數是值傳遞,那么這里有個問題需要考慮: 當把一個slice變量通過參數傳遞給某函數時,傳的是SliceHeader、還是整個SliceHeader+數據(存儲數據的數組)都被復制過去?
比如,這樣的代碼:
s := make([]string, 10) saveSlice(s)
當我們在項目中某個slice有10萬元素,如果傳參數直接復制SliceHeader + 數據
,那么這是一定不能接受的。
Rob Pike有這樣一句話定義Slice: slice不是數組,它是對數組進行了描述(A slice is not an array. A slice describes a piece of an array)
。 實際上,在上面的代碼片段中saveSlice(s)接收到的是變量s
的一個副本(就是一個值跟s
一樣,但是是全新的變量),這個副本跟變量s
一樣,有一個指向同個數組的指針、len和cap相同值。
為什么?因為Go是值傳遞,簡單試驗就知道。
實驗一:如果slice變量參數傳遞,是復制了數據
,那么在函數中操作"被復制過來的"數據,不會對原數據造成影響。
// 代碼片段 data := make([]int, 10) fmt.Println("處理前數據: ", data) changeIndex0(data) fmt.Println("處理后數據: ", data)
函數 changeIndex0(data []int)
// 替換第一個元素的值 func changeIndex0(data []int) { data[0] = 99 }
實驗結果
處理前數據: [0 0 0 0 0 0 0 0 0 0] 處理后數據: [99 0 0 0 0 0 0 0 0 0]
顯然,從結果中看得出,原始數據被修改了,所以可以得出結論是:傳遞slice變量時,并不是復制真正存儲數據的數組進行傳遞。
所以,在實際項目中,直接傳遞slice變量與傳遞slice變量的指針,對內存的消耗區別并不是很大。一個SliceHeader的大小是24字節,而指針大小8字節。
備注: SliceHeader 24字節計算方式:8字節(指針) + 8字節(整型int, len) + 8字節(整型int, cap),這是以我自己電腦為例(64位),指針大小8字節;整型int大小也跟編譯器有關,但Golang中最少是32bit,我在本機使用unsafe.SizeOf()實測是8字節。
2.2 Slice截取和擴充
說到底,slice由SliceHeader和數組構成。涉及到數組,避不開的問題就是定長
,也就是一旦數組長度確定了就無法改變。如果非要改變長度,那只能一個辦法:重新分配一個新的數組。
對于slice也一樣,如果一個slice已經確定了容量(capacity),那么如果要擴充該slice的容量,也必須重新分配一個存數據的數組。
備注:slice的容量在使用make([]byte, 10, 20)時,第三個參數已經確定;第三個參數就是容量(capacity),如果不指定,默認跟第二個參數(長度len)一樣。
i. 截取子Slice
當基于原Slice進行截取子Slice時,實際上操作的還是原Slice的元素。也就是對子Slice的元素進行修改,都會在原Slice中體現。
實驗 二:操作從原 Slice 截取而獲得的子 Slice
// 代碼片段 data := make([]int, 10) fmt.Println("處理前數據: ", data, len(data), cap(data)) subSlice(data) fmt.Println("處理后數據: ", data, len(data), cap(data))
函數 subSlice(data []int)
// 截取slice func subSlice(data []int) { data[0] = 99 data = data[0:8] fmt.Println("函數中數據: ", data, len(data), cap(data)) }
實驗結果
處理前數據: [0 0 0 0 0 0 0 0 0 0] 10 10 函數中數據: [99 0 0 0 0 0 0 0] 8 10 處理后數據: [99 0 0 0 0 0 0 0 0 0] 10 10
從實驗結果可以看出,在Slice的容量(capacity)范圍內子Slice截取,是直接使用了原Slice的數組,并沒有為該子Slice分配新的數組。
如果我需要截取一個子Slice并且希望該子Slice有新的數組,該怎么操作?這是可以使用copy()函數。
sub := make([]int, 2) copy(sub, data[3:5])
ii. 擴充Slice
事實上,擴充Slice的操作就是:重新創建一個更大容量的Slice,然后把原Slice中的數據復制到新的Slice里面。
比如:常用操作append()
fmt.Printf("append()前: len: %d, cap: %d \n", len(data), cap(data)) data = append(data, 5) fmt.Printf("append()后: len: %d, cap: %d \n", len(data), cap(data))
結果
append()前: len: 10, cap: 10 append()后: len: 11, cap: 20
append()中的操作就是新建了一個容量為原來兩倍的Slice,然后把原來的數據復制到新Slice并且把新的元素加上。
3. Slice 常用操作函數
Go提供了方便操作的語法糖,如: data[2:5],以此來獲取第二到第四(包括第四)個元素。
備注: ':' 左右都可以不指定值。右邊的值不可以超過該Slice的容量大小,否則會Panic。
3.1 copy()
復制Slice的值到另外一個Slice,上面例子也用到了,這函數會自動參考len更小的那個Slice,不會發生報slice bounds out of range
的異常。
3.2 append()
給某個Slice添加元素,也是常用的,上面的例子也有體現。
4. 其他
4.1 關于string
從源碼包runtime
中string.go
中可以看到字符串的struct。
type stringStruct struct { str unsafe.Pointer len int }
也就是,string實際上就是只讀的byte切片(Slice),只是從Golang語言層面提供的語法支持而已。因為只能讀,所以容量的存在與否都無濟于事。
4.2 關于Slice nil
我們知道make()
方法專門用來新建Slice、map、chan,但是我們也可以用new()
來建Slice,但是兩者有區別。
// 代碼片段 nilSlice := new([]int) fmt.Printf("nilSlice is nil: %v \n", *nilSlice == nil) emptySlice := make([]int, 0) fmt.Printf("emptySlice is nil: %v \n", emptySlice == nil)
結果打印
nilSlice is nil: true emptySlice is nil: false
也就是用new()
創建后的Slice變量是零值,而make()創建一個0長度的Slice并不是nil。
為什么?因為new()
和 make()
做的事情不一樣。
new()
做了兩件事
為該類型分配內存
置零值(不同類型的零值不一樣,比如: bool是false,整型是0...等)
make()
也做了兩件事
為該類型分配內存
初始化
以Slice為例,new([]int)
得到的SliceHeader是:
sliceHeader { array: nil, len: 0, cap: 0, }
而make([]int, 0)
得到的SliceHeader應該是:
sliceHeader { array: 0x8201d0140, // 指向0個元素的數組 len: 0, cap: 0, }
5. 結語
5. 結語
從Slice的實現、使用場景進行更加全面的了解,會對在項目中的使用有更大的幫助以及盡量避免因為不知道細節而錯用。
參考:
[1] Golang/slices
掃描二維碼推送至手機訪問。
版權聲明:本文由短鏈接發布,如需轉載請注明出處。
本文鏈接:http://www.virginiabusinesslawupdate.com/article_316.html