前言
通過上一篇博文,我們學習了陣列的使用,但是陣列有一個致命的缺陷,那就是大小固定,這個特性並不能滿足我們平時的開發需求,所以Go的切片由此誕生。
切片的記憶體分佈是連續的,所以你可以把切片當做一個大小不固定的陣列。
切片有三個欄位的資料結構,這些資料結構包含Go 語言需要操作底層陣列的後設資料,這 3 個欄位分別是指向底層陣列的指標、切片訪問的元素的個數(即長度)和切片允許增長到的元素個數(即容量)。後面會進一步講解長度和容量的區別。
建立和初始化切片
切片的建立有多種方式,下面我們一一來講解。
使用make建立切片
//建立了一個長度和容量都為5的切片
slice1:=make[[]string,5]
//建立了一個長度5,容量為10的切片
slice2:=make[[]string,5,10]
需要說明的是,切片對應的底層陣列的大小為指定的容量,這個一定要謹記,比如對於上面的例子,我們指定了slice2
的容量為10,那麼slice2
對應的底層陣列的大小就是10。
上面雖然建立的切片對應底層陣列的大小為10,但是你不能訪問索引值5以後的元素,比如,如果你執行以下程式碼,你就會發現:
func main() {
slice := make([]int, 4, 10)
fmt.Println(slice[2])
fmt.Println(slice[6])
}
執行結果如下:
0
panic: runtime error: index out of range
goroutine 1 [running]:
main.main()
E:/go-source/go-arr/main.go:19 +0x8d
Process finished with exit code 2
通過切片字面量來建立切片
func main() {
slice := []int{1, 2, 4, 4}
}
建立陣列和建立切片非常相似,如果你在[]指定了值,那麼建立的是一個陣列,反之就是一個切片。
切面字面量也可以指定切片的大小和容量,如下所示:
func main() {
slice := []int{99: 100}
}
上面建立的切片的大小和容量都為100,並且初始化第100個元素的值為100,只是在這種情況下,容量和長度是相等的。
建立空切片
func main() {
slice1 := []int{}
slice2 := make([]int, 0)
}
空切片在底層陣列包含0 個元素,也沒有分配任何儲存空間。想表示空集合時空切片很有用。
切片的使用
切片的使用和陣列是一模一樣的:
func main() {
slice1 := []int{1,2,3,4}
fmt.Println(slice1[1])
}
切片建立切片
切片之所以稱為切片,是因為它只是對應底層陣列的一部分,看如下所示程式碼:
func main() {
slice := []int{10, 20, 30, 40, 50}
newSlice := slice[1:3]
}
為了說明上面的程式碼,我們看下面的這張圖:
第一個切片slice 能夠看到底層陣列全部5 個元素的容量,不過之後的newSlice 就看不到。對於newSlice,底層陣列的容量只有4 個元素。newSlice 無法訪問到它所指向的底層陣列的第一個元素之前的部分。所以,對newSlice 來說,之前的那些元素就是不存在的。
需要記住的是,現在兩個切片共享同一個底層陣列。如果一個切片修改了該底層陣列的共享部分,另一個切片也能感知到,執行下面的程式碼:
func main() {
slice := []int{10, 20, 30, 40, 50}
newSlice := slice[1:3]
slice[1] = 200
fmt.Println(newSlice[0])
}
執行結果如下:
200
切片只能訪問到其長度內的元素。試圖訪問超出其長度的元素將會導致語言執行時異常,比如對上面的newSlice
,他只能訪問索引為1和2的元素(不包括3),比如:
func main() {
slice := []int{10, 20, 30, 40, 50}
newSlice := slice[1:3]
fmt.Println(newSlice[3])
}
執行程式碼,控制檯會報錯:
panic: runtime error: index out of range
goroutine 1 [running]:
main.main()
E:/go-source/go-arr/main.go:20 +0x11
子切片的容量
我們知道切片可以再生出切片,那麼子切片的容量為多大呢?我們來測試一下:
func main() {
slice := make([]int, 2, 10)
slice1 := slice[1:2]
fmt.Println(cap(slice1))
}
控制檯列印結果為:
9
9
從結果我們可以推測,子切片的容量為底層陣列的長度減去切片在底層陣列的開始偏移量,比如在上面的例子中,slice1的偏移值為1,底層陣列的大小為10,所以兩者相減,得到結果9。
向切片中追加元素
go提供了append
方法用於向切片中追加元素,如下所示:
func main() {
slice := make([]int, 2, 10)
slice1 := slice[1:2]
slice2 := append(slice1, 1)
slice2[0] = 10001
fmt.Println(slice)
fmt.Println(cap(slice2))
}
輸出結果如下:
[0 10001]
9
此時slice,slice1,slice2共享底層陣列,所以只要一個切片改變了某一個索引的值,會影響到所有的切片,還有一點值得注意,就是slice2的容量為9,記住這個值。
為了說明問題,我把例子改為如下所示程式碼:
func main() {
slice := make([]int, 2, 10)
slice1 := slice[1:2]
slice2 := append(slice1, 1)
slice2 = append(slice2, 1)
slice2 = append(slice2, 1)
slice2 = append(slice2, 1)
slice2 = append(slice2, 1)
slice2 = append(slice2, 1)
slice2 = append(slice2, 1)
slice2 = append(slice2, 1)
slice2 = append(slice2, 1)
slice2 = append(slice2, 1)
slice2 = append(slice2, 1)
slice2 = append(slice2, 1)
slice2[0] = 10001
fmt.Println(slice)
fmt.Println(slice1)
fmt.Println(cap(slice2))
}
此時我們再次列印結果,神奇的事情出現了:
[0 0]
[0]
18
雖然我們改變0位置的值,但是並沒有影響到原來的slice和slice1,這是為啥呢?我們知道原始的slice2對應的底層陣列的容量為9,經過我們一系列的append操作,原始的底層陣列已經無法容納更多的元素了,此時Go會分配另外一塊記憶體,把原始切片從位置1開始的記憶體複製到新的記憶體地址中,也就是說現在的slice2切片對應的底層陣列和slice切片對應的底層陣列完全不是在同一個記憶體地址,所以當你此時更改slice2中的元素時,對slice已經來說,一點兒關係都沒有。
另外根據上面的列印結果,你也應該猜到了,當切片容量不足的時候,Go會以原始切片容量的2倍建立新的切片,在我們的例子中2*9=18,就是這麼粗暴。
如何建立子切片時指定容量
在前面的例子中,我們建立子切片的時候,沒有指定子切片的容量,所以子切片的容量和我們上面討論的計運算元切片的容量方法相等,那麼我們如何手動指定子切片的容量呢?
在這裡我們借用《Go實戰》中的一個例子:
func main() {
source := []string{"Apple", "Orange", "Plum", "Banana", "Grape"}
slice := source[2:3:4]
fmt.Println(cap(slice))
}
如果你仔細看的話,上面的子切片的生成方式和普通的切片有所不同,[]裡面有三個部分組成,,第一個值表示新切片開始元素的索引位置,這個例子中是2。第二個值表示開始的索引位置(2)加上希望包括的元素的個數(1),2+1 的結果是3,所以第二個值就是3。為了設定容量,從索引位置2 開始,加上希望容量中包含的元素的個數(2),就得到了第三個值4。所以這個新的切片slice的長度為1,容量為2。還有一點大家一定要記住,你指定的容量不能比原先的容量,這裡就是source的容量大,加入我們這樣設定的話:
func main() {
source := []string{"Apple", "Orange", "Plum", "Banana", "Grape"}
slice := source[2:3:10]
fmt.Println(cap(slice))
}
執行結果如下,報錯了,哈哈:
panic: runtime error: slice bounds out of range [::10] with capacity 5
goroutine 1 [running]:
main.main()
E:/learn-go/slice/main.go:7 +0x1d
迭代切片
關於如何迭代切片,我們可以使用range配置來使用,如下:
func main() {
slice:=[]int{1,2,4,6}
for _, value:=range slice{
fmt.Println(value)
}
}
關於迭代切片,大家有一點需要注意,就以上面的例子為例,value只是slice中元素的副本,為啥呢?我們來驗證這一點:
func main() {
slice:=[]int{1,2,4,6}
for index, value:=range slice{
fmt.Printf("value[%d],indexAddr:[%X],valueAddr:[%X],sliceAddr:[%X]\n",value,&index,&value,&slice[index])
}
}
控制檯列印結果如下:
value[1],indexAddr:[C00000A0B8],valueAddr:[C00000A0D0],sliceAddr:[C000010380]
value[2],indexAddr:[C00000A0B8],valueAddr:[C00000A0D0],sliceAddr:[C000010388]
value[4],indexAddr:[C00000A0B8],valueAddr:[C00000A0D0],sliceAddr:[C000010390]
value[6],indexAddr:[C00000A0B8],valueAddr:[C00000A0D0],sliceAddr:[C000010398]
從上面的結果可以看到index和value的地址始終是不變的,所以它們始終是同一個變數,只是變數引用地址的內容發生了變化,從而驗證迭代的時候,只能是切片元素的副本,最後看看sliceAddr代表的地址相隔8個位元組,因為在64位系統上,每一個int型別的大小為8個位元組。
函式間傳遞切片
函式間傳遞切片,也是以值的方式傳遞的,但是你還記得這篇博文開頭給出的切片的佈局麼?
切片由三個部分組成,包括指向底層陣列的指標,當前切片的長度,當前切片的容量,所以切片本身並不大,我們來測試一個切片的大小:
func main() {
slice:=[]int{1,2,4,6}
fmt.Println(unsafe.Sizeof(slice))
}
測試結果為:
24
也就是這個slice切片的大小為24位元組,所以當切片作為引數傳遞的時候,幾乎沒有效能開銷,還有很重要的一點,引數生成的副本的地址指標和原始切片的地址指標是一樣的,因此,如果你在函式裡面修改了切片,那麼會影響到原始的切片,我們來驗證這點:
func main() {
slice:=[]int{1,2,4,6}
handleSlice(slice)
fmt.Println(slice)
}
列印結果:
[100 2 4 6]
總結
這篇博文詳細的講敘了切片的使用和要點,希望大家謹記,當然了,最重要的是一定要理解。