前言
切片是一種複合資料型別,與陣列類似,存放相同資料型別的元素,但陣列的大小是固定的,而切片的大小可變,可以按需自動改變大小。切片是基於底層陣列實現的,是對陣列的抽象。切片很小,只有三個欄位的資料結構:指向底層陣列的指標、能訪問的元素個數(即切片長度)和允許增長到的元素個數(即切片容量)。
如上圖所示,一個長度為3、容量為5的整型切片的底層結構。宣告與初始化
make()建立
使用內建函式建立空切片,形如:
s := make([]type, len, cap) // len 長度,cap 容量
複製程式碼
也可以只指定len
,那麼切片的容量和長度是一樣的。Go語言提供了內建函式len
、cap
分別返回切片的長度和容量。
// 宣告一個長度為3、容量為5的整型切片
s1 := make([]int,3,5)
fmt.Println(len(s1),cap(s1)) // 輸出:3 5
// 宣告一個長度和容量都是5的字串切片
s2 := make([]string,5)
fmt.Println(len(s2),cap(s2)) // 輸出:5 5
複製程式碼
切片建立完成,如果不指定字面量的話,預設值就是陣列的元素的零值。
切片的容量就是切片底層陣列的大小,我們只能訪問切片長度範圍內的元素,如第一節的圖所示,長度為3的整型切片存入3個值後的結構,我們只能訪問到第3個元素,剩餘的2個元素需要切片擴充以後才可以訪問。所以,很明顯的:容量>=長度,我們不能建立長度大於容量的切片。
s1 := make([]int,5,3)
// 報錯:len larger than cap in make([]int)
複製程式碼
使用字面量建立切片
使用字面量建立,就是指定了初始化的值
s := []int{1,2,3,4,5} // 長度和容量都是5的整型切片
複製程式碼
有沒有發現,這種建立方式與建立陣列類似,只不過不用指定[]
的值,這時候切片的長度和容量是相等的,並且會根據指定的字面量推匯出來。
區別:
// 建立大小為10的陣列
s := [10]int{1,2,3,4,5}
// 建立切片
s := []int{1,2,3,4,5}
複製程式碼
我們也可以只初始化某一個索引的值:
s := []int{4:1}
fmt.Println(len(s),cap(s)) // 輸出:5 5
fmt.Println(s) // 輸出:[0 0 0 0 1]
複製程式碼
指定了第5個元素為1
,其他元素初始化為0。
基於已有的陣列或者切片建立切片
使用操作符[start:end]
,簡寫成[i:j]
,表示從索引i
,到索引j
結束,擷取已有陣列或者切片的任意部分,返回一個新的切片,新切片的值包含原切片的i
索引的值,但是不包含j
索引的值。i
、j
都是可選的,i
如果省略,預設是0,j
如果省略,預設是原切片或陣列的長度。i
、j
都不能超過這個長度值。
s := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
fmt.Println("s[:]", s[:])
fmt.Println("s[2:]", s[2:])
fmt.Println("s[:4]", s[:4])
fmt.Println("s[2:4]", s[2:4])
複製程式碼
輸出
你可能會有個疑問:擷取獲得的新切片的長度和容量怎麼計算呢?我們當然可以使用內建函式len
、cap
直接獲得,如果明白了怎麼計算的,我們處理問題就可以更得心應手。
對底層陣列大小為k
的切片執行[i,j]
操作之後獲得的新切片的長度和容量是:
長度:j-i
容量:k-i
就拿上一個例子的s[2:4]
來說,原切片底層陣列大小是10,所以新切片的長度是4-2=2
,容量是10-2=8
。
可以使用內建函式驗證下:
s1 := s[2:4]
fmt.Println(len(s1),cap(s1)) // 輸出:2 8
複製程式碼
上面是使用2個索引的方法建立切片,還可以使用3個索引的方法,第3個用來限定新切片的容量,用法為slice[i:j:k]
。
s2 := s[2:4:8]
fmt.Println(s2) // 輸出:[2 3]
複製程式碼
長度和容量如何計算:長度j-i
,容量k-i
。所以切片s2
的長度和容量分別是2
、6
。注意:k
不能超過原切片(陣列)的長度,否則報錯panic: runtime error: slice bounds out of range
。
例如上面的例子中,第三個索引值不能超過10。
我們來看個例子:
s := []int{0, 1, 2, 3, 4, 5}
fmt.Println("before,s:",s)
s1 := s[1:4]
fmt.Println("before,s1:",s1)
s1[1] = 10
fmt.Println("after,s1:",s1)
fmt.Println("after,s:",s)
複製程式碼
輸出:
before,s: [0 1 2 3 4 5]
before,s1: [1 2 3]
after,s1: [1 10 3]
after,s: [0 1 10 3 4 5]
複製程式碼
這個例子說明,原切片和新切片是基於同一個底層陣列的,所以當修改的時候,底層陣列的值就會被改變,原切片的值也隨之改變了。對於基於陣列的切片也一樣的。
我們可以看到,執行完切片動作之後,獲得一個新切片,與原切片共享同一段底層陣列,但通過不同的切片會看到底層陣列的不同部分。切片s
能夠看到底層陣列全部6個元素,而切片s1
只能看到索引1
及之後的全部元素,對於s1
來說,索引1
之前的部分是不存在。
使用切片
切片的使用方法與陣列的使用方法類似,直接通過索引就可以獲取、修改元素的值。
s := []int{1, 2, 3, 4, 5}
fmt.Println(s[1]) // 獲取值 輸出:2
s[1] = 10 // 修改值
fmt.Println(s) //輸出:[1 10 3 4 5]
複製程式碼
只能訪問切片長度範圍內的元素,否則報錯
s := []int{1, 2, 3, 4, 5}
s1 := s[2:3]
fmt.Println(s1[1])
複製程式碼
上面這個例子中,s1
的容量為3,長度為1,所以只能訪問s1
第一個元素s1[0]
,訪問s1[1]
就會報錯:panic: runtime error: index out of range
與切片的容量相關聯的元素只能用於增長切片,在使用這部分元素前,必須將其合併到切片的長度裡。
相較於陣列,使用切片的好處在於,可以按需增長,類似於動態陣列。Go提供了內建append
函式,能夠幫我們處理切片增長的一些列細節,我們只管使用就可以了。
函式原型:
func append(slice []Type, elems ...Type) []Type
複製程式碼
使用append
函式,需要一個被操作的切片和一個(多個)追加值,返回一個相同資料型別的新切片。
s := []int{1, 2, 3, 4, 5}
newS := s[2:4]
newS = append(newS, 50)
fmt.Println(s, newS)
fmt.Println(&s[2] == &newS[0])
複製程式碼
輸出:
[1 2 3 4 50] [3 4 50]
true
複製程式碼
上面的例子中,擷取獲得一個長度為2,容量為3(可用容量為1)的新切片newS
,通過append
函式向切片newS
追加一個元素50。
追加元素50之前:
newS
與原切片s
是共享底層陣列的,當切片可用容量能夠存下追加元素時,不會建立新的切片。當切片可用容量存不下需要追加的元素時會發生呢?
s := []int{1, 2, 3, 4, 5, 6, 7, 8}
s1 := s[2:4]
fmt.Printf("before -> s=%v\n", s)
fmt.Printf("before -> s1=%v\n", s1)
fmt.Printf("before -> len=%d, cap=%d\n", len(s1), cap(s1))
fmt.Println("&s[2] == &s1[0] is", &s[2] == &s1[0])
s1 = append(s1, 60, 70, 80, 90, 100, 110)
fmt.Printf("after -> s=%v\n", s)
fmt.Printf("after -> s1=%v\n", s1)
fmt.Printf("after -> len=%d, cap=%d\n", len(s1), cap(s1))
fmt.Println("&s[2] == &s1[0] is", &s[2] == &s1[0])
複製程式碼
輸出:
before -> s=[1 2 3 4 5 6 7 8]
before -> s1=[3 4]
before -> len=2, cap=6
&s[2] == &s1[0] is true
after -> s=[1 2 3 4 5 6 7 8]
after -> s1=[3 4 60 70 80 90 100 110]
after -> len=8, cap=12
&s[2] == &s1[0] is false
複製程式碼
追加元素60、70、80、90、100和110之前:
追加之後: 從結果可以看出,切片的底層陣列沒有足夠的可用容量,append
函式會建立一個新的底層陣列,將原陣列的值複製到新陣列裡,再追加新的值,就不會影響原來的底層陣列。
一般我們在建立新切片的時候,最好要讓新切片的長度和容量一樣,這樣我們在追加操作的時候就會生成新的底層陣列,和原有陣列分離,就不會因為共用底層陣列而引起奇怪問題,因為共用陣列的時候修改內容,會影響多個切片。
append
函式會智慧地增加底層陣列的容量,目前的演算法是:當陣列容量<=1024
時,會成倍地增加;當超過1024
,增長因子變為1.25,也就是說每次會增加25%的容量。
Go提供了...
操作符,允許將一個切片追加到另一個切片上:
s := []int{1, 2,3,4,5}
s1 := []int{6,7,8}
s = append(s,s1...)
fmt.Println(s,s1)
複製程式碼
輸出:
[1 2 3 4 5 6 7 8] [6 7 8]
複製程式碼
迭代切片
使用for
迴圈迭代切片,配合len
函式使用:
s := []int{1, 2, 3, 4, 5}
for i:=0;i<len(s) ;i++ {
fmt.Printf("Index:%d,Value:%d\n",i,s[i])
}
複製程式碼
使用for range
迭代切片:
s := []int{1, 2, 3, 4, 5}
for i,v := range s {
fmt.Printf("Index:%d,Value:%d\n",i,v)
}
// 使用‘_’可以忽略返回值
s := []int{1, 2, 3, 4, 5}
for _,v := range s {
fmt.Printf("Value:%d\n",v)
}
複製程式碼
需要注意的是,range
返回的是切片元素的複製,而不是元素的引用。如果使用該值變數的地址作為指向每個元素的指標,就會造成錯誤。
s := []int{1, 2, 3, 4, 5}
for i,v := range s {
fmt.Printf("v:%d,v_addr:%p,elem_addr:%p\n",v,&v,&s[i])
}
複製程式碼
輸出:
v:1,v_addr:0xc000018058,elem_addr:0xc000016120
v:2,v_addr:0xc000018058,elem_addr:0xc000016128
v:3,v_addr:0xc000018058,elem_addr:0xc000016130
v:4,v_addr:0xc000018058,elem_addr:0xc000016138
v:5,v_addr:0xc000018058,elem_addr:0xc000016140
複製程式碼
可以看到,v
的地址總是相同的,因為迭代返回的變數在迭代過程中根據切片依次賦值的新變數。
好了,今天先講到這裡,下一節,我們再來討論關於Slice
更多的用法!
(全文完)
原創文章,若需轉載請註明出處!
歡迎掃碼關注公眾號「Golang來啦」或者移步 seekload.net ,檢視更多精彩文章。
公眾號「Golang來啦」給你準備了一份神祕學習大禮包,後臺回覆【電子書】領取!