Go基礎系列(6):細說slice結構
slice表示切片(分片),例如對一個數組進行切片,取出陣列中的一部分值。在現代程式語言中,slice(切片)幾乎成為一種必備特性,它可以從一個數組(列表)中取出任意長度的子陣列(列表),為操作資料結構帶來非常大的便利性,如python、perl等都支援對陣列的slice操作,甚至perl還支援對hash資料結構的slice。
但Go中的slice和這些語言的slice不太一樣,前面所說的語言中,slice是一種切片的操作,切片後返回一個新的資料物件。而Go中的slice不僅僅是一種切片動作,還是一種資料結構(就像陣列一樣)。
slice的儲存結構
Go中的slice依賴於陣列,它的底層就是陣列,所以陣列具有的優點,slice都有。且slice支援可以通過append向slice中追加元素,長度不夠時會動態擴充套件,通過再次slice切片,可以得到得到更小的slice結構,可以迭代、遍歷等。
實際上slice是這樣的結構:先建立一個有特定長度和資料型別的底層陣列,然後從這個底層陣列中選取一部分元素,返回這些元素組成的集合(或容器),並將slice指向集合中的第一個元素。換句話說,slice自身維護了一個指標屬性,指向它底層陣列中的某些元素的集合。
例如,初始化一個slice資料結構:
my_slice := make([]int, 3,5)
// 輸出slice
fmt.Println(my_slice) // 輸出:[0 0 0]
這表示先宣告一個長度為5、資料型別為int的底層陣列,然後從這個底層陣列中從前向後取3個元素(即index從0到2)作為slice的結構。
如下圖:
每一個slice結構都由3部分組成:容量(capacity)、長度(length)和指向底層陣列某元素的指標,它們各佔8位元組,所以任何一個slice都是24位元組
- Pointer:表示該slice結構從底層陣列的哪一個元素開始,該指標指向該元素
- Capacity:即底層陣列的長度,表示這個slice目前最多能擴充套件到這麼長
- Length:表示slice當前的長度,如果追加元素,長度不夠時會擴充套件,最大擴充套件到Capacity的長度(不完全準確,後面陣列自動擴充套件時解釋),所以Length必須不能比Capacity更大,否則會報錯
對上面建立的slice來說,它的長度為3,容量為5,指標指向底層陣列的index=0。
可以通過len()函式獲取slice的長度,通過cap()函式獲取slice的Capacity。
my_slice := make([]int,3,5) fmt.Println(len(my_slice)) // 3 fmt.Println(cap(my_slice)) // 5
還可以直接通過print()或println()函式去輸出slice,它將得到這個slice結構的屬性值,也就是length、capacity和pointer:
my_slice := make([]int,3,5)
println(my_slice) // [3/5]0xc42003df10
[3/5]
表示length和capacity,0xc42003df10
表示指向的底層陣列元素的指標。
務必記住slice的本質是[x/y]0xADDR
,記住它將在很多地方有助於理解slice的特性。另外,個人建議,雖然slice的本質不是指標,但仍然可以將它看作是一種包含了另外兩種屬性的不純粹的指標,也就是說,直接認為它是指標。其實不僅slice如此,map也如此。
建立、初始化、訪問slice
有幾種建立slice資料結構的方式。
一種是使用make():
// 建立一個length和capacity都等於5的slice
slice := make([]int,5)
// length=3,capacity=5的slice
slice := make([]int,3,5)
make()比new()函式多一些操作,new()函式只會進行記憶體分配,而make()可以先為底層陣列分配好記憶體,然後從這個底層陣列中再額外生成一個slice並初始化。
還可以直接賦值初始化的方式建立slice:
// 建立長度和容量都為4的slice,並初始化賦值
color_slice := []string{"red","blue","black","green"}
// 建立長度和容量為100的slice,併為第100個元素賦值為3
slice := []int{99:3}
注意區分array和slice:
// 建立長度為3的int陣列
array := [3]int{10, 20, 30}
// 建立長度和容量都為3的slice
slice := []int{10, 20, 30}
由於slice底層是陣列,所以可以使用索引的方式訪問slice,或修改slice中元素的值:
// 建立長度為5、容量為5的slice
my_slice := []int{11,22,33,44,55}
// 訪問slice的第2個元素
print(my_slice[1])
// 修改slice的第3個元素的值
my_slice[2] = 333
由於slice的底層是陣列,所以訪問my_slice[1]
實際上是在訪問它的底層陣列的對應元素。slice能被訪問的元素只有length範圍內的元素,那些在length之外,但在capacity之內的元素暫時還不屬於slice,只有在slice被擴充套件時(見下文append),capacity中的元素才被納入length,才能被訪問。
nil slice和空slice
當宣告一個slice,但不做初始化的時候,這個slice就是一個nil slice。
// 宣告一個nil slice
var nil_slice []int
nil slice表示它的指標為nil,也就是這個slice不會指向哪個底層陣列。也因此,nil slice的長度和容量都為0。
|--------|---------|----------|
| nil | 0 | 0 |
| ptr | Length | Capacity |
|--------|---------|----------|
還可以建立空slice(Empty Slice),空slice表示長度為0,容量為0,但卻有指向的slice,只不過指向的底層陣列暫時是長度為0的空陣列。
// 使用make建立
empty_slice := make([]int,0)
// 直接建立
empty_slice := []int{}
Empty Slice的結構如下:
|--------|---------|----------|
| ADDR | 0 | 0 |
| ptr | Length | Capacity |
|--------|---------|----------|
雖然nil slice和Empty slice的長度和容量都為0,輸出時的結果都是[]
,且都不儲存任何資料,但它們是不同的。nil slice不會指向底層陣列,而空slice會指向底層陣列,只不過這個底層陣列暫時是空陣列。
當然,無論是nil slice還是empty slice,都可以對它們進行操作,如append()函式、len()函式和cap()函式。
對slice進行切片
可以從slice中繼續切片生成一個新的slice,這樣能實現slice的縮減。
對slice切片的語法為:
SLICE[A:B]
SLICE[A:B:C]
其中A表示從SLICE的第幾個元素開始切,B控制切片的長度(B-A),C控制切片的容量(C-A),如果沒有給定C,則表示切到底層陣列的最尾部。
例如:
my_slice := []int{11,22,33,44,55}
// 生成新的slice,從第二個元素取,切取的長度為2
new_slice := my_slice[1:3]
注意,擷取時"左閉右開"。所以上面new_slice
是從my_slice
的index=1開始擷取,擷取到index=3為止,但不包括index=3這個元素。所以,新的slice是由my_slice
中的第2個元素、第3個元素組成的新的資料結構,長度為2。
以下是slice切片生成新的slice後的結構:
不難發現,一個底層陣列,可以生成無數個slice,且對於new_slice而言,它並不知道底層陣列index=0的那個元素。
還可以控制切片時新slice的容量:
my_slice := []int{11,22,33,44,55}
// 從第二個元素取,切取的長度為2,容量也為2
new_slice := my_slice[1:3:3]
這時新slice的length等於capacity,底層陣列的index=4、5將對new_slice永不可見,即使後面對new_slice進行append()導致底層陣列擴容也仍然不可見。具體見下文。
由於多個slice共享同一個底層陣列,所以當修改了某個slice中的元素時,其它包含該元素的slice也會隨之改變,因為slice只是一個指向底層陣列的指標(只不過這個指標不純粹,多了兩個額外的屬性length和capacity),實際上修改的是底層陣列的值,而底層陣列是被共享的。
當同一個底層陣列有很多slice的時候,一切將變得混亂不堪,因為我們不可能記住誰在共享它,通過修改某個slice的元素時,將也會影響那些可能我們不想影響的slice。所以,需要一種特性,保證各個slice的底層陣列互不影響,相關內容見下面的"擴容"。
append()函式
可以使用append()函式對slice進行擴充套件,因為它追加元素到slice中,所以一定會增加slice的長度。
但必須注意,append()的結果必須被使用。所謂被使用,可以將其輸出、可以賦值給某個slice。如果將append()放在空上下文將會報錯:append()已評估,但未使用。同時這也說明,append()返回一個新的slice,原始的slice會保留不變。
例如:
my_slice := []int{11,22,33,44,55}
new_slice := my_slice[1:3]
// append()追加一個元素2323,返回新的slice
app_slice := append(new_slice,2323)
上面的append()在new_slice
的後面增加了一個元素2323,所以app_slice[2]=2323
。但因為這些slice共享同一個底層陣列,所以2323也會反映到其它slice中。
現在的資料結構圖如下:
當然,如果append()的結果重新賦值給new_slice,則new_slice
會增加長度。
擴容
當slice的length已經等於capacity的時候,再使用append()給slice追加元素,會自動擴充套件底層陣列的長度。
底層陣列擴充套件時,會生成一個新的底層陣列。所以舊底層陣列仍然會被舊slice引用,新slice和舊slice不再共享同一個底層陣列。
func main() {
my_slice := []int{11,22,33,44,55}
new_slice := append(my_slice,66)
my_slice[3] = 444 // 修改舊的底層陣列
fmt.Println(my_slice) // [11 22 33 444 55]
fmt.Println(new_slice) // [11 22 33 44 55 66]
fmt.Println(len(my_slice),":",cap(my_slice)) // 5:5
fmt.Println(len(new_slice),":",cap(new_slice)) // 6:10
}
從上面的結果上可以發現,底層陣列被擴容為10,且是新的底層陣列。
實際上,當底層陣列需要擴容時,會按照當前底層陣列長度的2倍進行擴容,並生成新陣列。如果底層陣列的長度超過1000時,將按照25%的比率擴容,也就是1000個元素時,將擴充套件為1250個,不過這個增長比率的演算法可能會隨著go版本的遞進而改變。
實際上,上面的說法應該改一改:當capacity需要擴容時,會按照當前capacity的2倍對陣列進行擴容。或者說,是按照slice的本質[x/y]0xADDR
的容量y來判斷如何擴容的。之所以要特別強調這兩種不同,是因為很容易搞混。
例如,擴容的物件是底層陣列的真子集時:
my_slice := []int{11,22,33,44,55}
// 限定長度和容量,且讓長度和容量相等
new_slice := my_slice[1:3:3] // [22 33]
// 擴容
app_slice := append(new_slice,4444)
上面的new_slice
的容量為2,並沒有對應到底層陣列的最結尾,所以new_slice
是my_slice
的一個真子集。這時對new_slice
擴容,將生成一個新的底層陣列,新的底層陣列容量為4,而不是10。如下圖:
因為建立了新的底層陣列,所以修改不同的slice,將不會互相影響。為了保證每次都是修改各自的底層陣列,通常會切出僅一個長度、僅一個容量的新slice,這樣只要對它進行任何一次擴容,就會生成一個新的底層陣列,從而讓每個slice的底層陣列都獨立。
my_slice := []int{11,22,33,44,55}
new_slice := my_slice[2:3:3]
app_slice := append(new_slice,3333)
事實上,這還是會出現共享的機率,因為沒有擴容時,那個唯一的元素仍然是共享的,修改它肯定會影響至少1個slice,只有切出長度為0,容量為0的slice,才能完全保證獨立性,但這和新建立一個slice沒有區別。
合併slice
slice和陣列其實一樣,都是一種值,可以將一個slice和另一個slice進行slice,生成一個新的slice。
合併slice時,只需將append()的第二個引數後加上...
即可,即append(s1,s2...)
表示將s2合併在s1的後面,並返回新的slice。
s1 := []int{1,2}
s2 := []int{3,4}
s3 := append(s1,s2...)
fmt.Println(s3) // [1 2 3 4]
注意append()最多允許兩個引數,所以一次性只能合併兩個slice。但可以取巧,將append()作為另一個append()的引數,從而實現多級合併。例如,下面的合併s1和s2,然後再和s3合併,得到s1+s2+s3
合併後的結果。
sn := append(append(s1,s2...),s3...)
slice遍歷迭代
slice是一個集合,所以可以進行迭代。
range關鍵字可以對slice進行迭代,每次返回一個index和對應的元素值。可以將range的迭代結合for迴圈對slice進行遍歷。
package main
func main() {
s1 := []int{11,22,33,44}
for index,value := range s1 {
println("index:",index," , ","value",value)
}
}
輸出結果:
index: 0 , value 11
index: 1 , value 22
index: 2 , value 33
index: 3 , value 44
傳遞slice給函式
前面說過,雖然slice實際上包含了3個屬性,它的資料結構類似於[3/5]0xc42003df10
,但仍可以將slice看作一種指標。這個特性直接體現在函式引數傳值上。
Go中函式的引數是按值傳遞的,所以呼叫函式時會複製一個引數的副本傳遞給函式。如果傳遞給函式的是slice,它將複製該slice副本給函式,這個副本實際上就是[3/5]0xc42003df10
,所以傳遞給函式的副本仍然指向源slice的底層陣列。
換句話說,如果函式內部對slice進行了修改,有可能會直接影響函式外部的底層陣列,從而影響其它slice。但並不總是如此,例如函式內部對slice進行擴容,擴容時生成了一個新的底層陣列,函式後續的程式碼只對新的底層陣列操作,這樣就不會影響原始的底層陣列。
例如:
package main
import "fmt"
func main() {
s1 := []int{11, 22, 33, 44}
foo(s1)
fmt.Println(s1[1]) // 輸出:23
}
func foo(s []int) {
for index, _ := range s {
s[index] += 1
}
}
上面將輸出23,因為foo()直接操作原始的底層陣列,對slice的每個元素都加1。