slice表示切片(分片),例如對一個數組進行切片,取出數組中的一部分值。在現代編程語言中,slice(切片)幾乎成爲一種必備特性,它能夠從一個數組(列表)中取出任意長度的子數組(列表),爲操做數據結構帶來很是大的便利性,如python、perl等都支持對數組的slice操做,甚至perl還支持對hash數據結構的slice。python
但Go中的slice和這些語言的slice不太同樣,前面所說的語言中,slice是一種切片的操做,切片後返回一個新的數據對象。而Go中的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]
這表示先聲明一個長度爲五、數據類型爲int的底層數組,而後從這個底層數組中從前向後取3個元素(即index從0到2)做爲slice的結構。架構
以下圖:app
每個slice結構都由3部分組成:容量(capacity)、長度(length)和指向底層數組某元素的指針,它們各佔8字節(1個機器字長,64位機器上一個機器字長爲64bit,共8字節大小,32位架構則是32bit,佔用4字節),因此任何一個slice都是24字節(3個機器字長)。編程語言
對上面建立的slice來講,它的長度爲3,容量爲5,指針指向底層數組的index=0。函數
能夠經過len()函數獲取slice的長度,經過cap()函數獲取slice的Capacity。ui
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數據結構的方式。
一種是使用make():
// 建立一個length和capacity都等於5的slice slice := make([]int,5) // length=3,capacity=5的slice slice := make([]int,3,5)
make()比new()函數多一些操做,new()函數只會進行內存分配並作默認的賦0初始化,而make()能夠先爲底層數組分配好內存,而後從這個底層數組中再額外生成一個slice並初始化。另外,make只能構建slice、map和channel這3種結構的數據對象,由於它們都指向底層數據結構,都須要先爲底層數據結構分配好內存並初始化。
還能夠直接賦值初始化的方式建立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的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,才能被訪問。
當聲明一個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會指向底層數組,只不過這個底層數組暫時是空數組。
可使用println()來輸出驗證:
package main func main() { var nil_s []int empty_s:= []int{} println(nil_s) println(empty_s) }
輸出結果:
[0/0]0x0 [0/0]0xc042085f50
固然,不管是nil slice仍是empty slice,均可以對它們進行操做,如append()函數、len()函數和cap()函數。
能夠從slice中繼續切片生成一個新的slice,這樣能實現slice的縮減。
對slice切片的語法爲:
SLICE[A:B] SLICE[A:B:C]
其中A表示從SLICE的第幾個元素開始切,B控制切片的長度(B-A),C控制切片的容量(C-A),若是沒有給定C,則表示切到底層數組的最尾部。
還有幾種簡化形式:
SLICE[A:] // 從A切到最尾部 SLICE[:B] // 從最開頭切到B(不包含B) SLICE[:] // 從頭切到尾,等價於複製整個SLICE
例如:
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=四、5將對new_slice永不可見,即便後面對new_slice進行append()致使底層數組擴容也仍然不可見。具體見下文。
因爲多個slice共享同一個底層數組,因此當修改了某個slice中的元素時,其它包含該元素的slice也會隨之改變,由於slice只是一個指向底層數組的指針(只不過這個指針不純粹,多了兩個額外的屬性length和capacity),實際上修改的是底層數組的值,而底層數組是被共享的。
當同一個底層數組有不少slice的時候,一切將變得混亂不堪,由於咱們不可能記住誰在共享它,經過修改某個slice的元素時,將也會影響那些可能咱們不想影響的slice。因此,須要一種特性,保證各個slice的底層數組互不影響,相關內容見下面的"擴容"。
能夠將一個slice拷貝到另外一個slice中。
$ go doc builtin copy func copy(dst, src []Type) int
這表示將src slice拷貝到dst slice,src比dst長,就截斷,src比dst短,則只拷貝src那部分。
copy的返回值是拷貝成功的元素數量,因此也就是src slice或dst slice中最小的那個長度。
例如:
s1 := []int{11, 22, 33} s2 := make([]int, 5) s3 := make([]int,2) num := copy(s2, s1) copy(s3,s1) fmt.Println(num) // 3 fmt.Println(s2) // [11,22,33,0,0] fmt.Println(s3) // [11,22]
此外,copy還能將字符串拷貝到byte slice中,由於字符串實際上就是[]byte
。
func main() { s1 := []byte("Hello") num := copy(s1, "World") fmt.Println(num) fmt.Println(s1) // 輸出[87 111 114 108 100 32] fmt.Println(string(s1)) //輸出"World" }
可使用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
會增長長度。
一樣,因爲string的本質是[]byte,因此string能夠append到byte slice中:
s1 := []byte("Hello") s2 := append(s1, "World"...) fmt.Println(string(s2)) // 輸出:HelloWorld
當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時,只需將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是一個集合,因此能夠進行迭代。
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實際上包含了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。
因爲slice的底層是數組,極可能數組很大,但slice所取的元素數量卻很小,這就致使數組佔用的絕大多數空間是被浪費的。
特別地,垃圾回收器(GC)不會回收正在被引用的對象,當一個函數直接返回指向底層數組的slice時,這個底層數組將不會隨函數退出而被回收,而是由於slice的引用而永遠保留,除非返回的slice也消失。
所以,當函數的返回值是一個指向底層數組的數據結構時(如slice),應當在函數內部將slice拷貝一份保存到一個使用本身底層數組的新slice中,並返回這個新的slice。這樣函數一退出,原來那個體積較大的底層數組就會被回收,保留在內存中的是小的slice。