切片
簡介
簡單地說,切片就是一種簡化版的動態數組。Go 數組的長度不可改變,而切片長度是不固定,切片的長度自然也就不能是類型的組成部分了。數組雖然有適用它們的地方,但是數組的類型和操作都不夠靈活,因此在Go代碼中數組使用的並不多。而切片則使用得相當廣泛,理解切片的原理和用法是一個Go程序員的必備技能。當進行append與copy函數操作時會對真實數據進行內存拷貝,append的時長度len超過申請的空間cap進行內存真實拷貝
初始化
package main import "fmt" func main() { // 第一種方式 對比數組不指定size大小 var slice1 = [] int{1,2,3,4} // 第二種方式 slice2 := [] int{1,2,3,4} // 第三種方式 make生成空切片 var slice3 []int = make([]int,3,5) // 第四種方式 簡寫 len=3 cap=5 slice4 := make([]int,3,5) fmt.Println(slice1) fmt.Println(slice2) fmt.Println(slice3) fmt.Println(slice4) } //[1 2 3 4] //[1 2 3 4] //[0 0 0] //[0 0 0]
數據結構
我們先看看切片的結構定義,reflect.SliceHeader
:
type SliceHeader struct { // 指向數組內存地址 賦值時拷貝的是數組地址 Data uintptr // 長度 Len int // 申請空間 Cap int }
可以看出切片的開頭部分和Go字符串是一樣的,但是切片多了一個Cap
成員表示切片指向的內存空間的最大容量(對應元素的個數,而不是字節數)。下圖是x := []int{2,3,5,7,11}
和y := x[1:3]
兩個切片對應的內存結構。
賦值、切片與copy
賦值等同於把結構體SliceHeader 內的變量拷貝了一份,並未進行真實數據拷貝, Data與初始切片指向的是同一塊內存地址
package main import "fmt" func main() { a := [] int{1,2,3,4} b := a // Data ptr指向的內存地址一致 fmt.Printf("a[0] %p\n",&a[0]) fmt.Printf("b[0] %p\n",&b[0]) // 修改元素值相互影響 b[0] = 10 fmt.Println(a) fmt.Println(b) } //a[0] 0xc000054140 //b[0] 0xc000054140 //[10 2 3 4] //[10 2 3 4]
切片時,新生成的切片Data指向初始切片位置元素的內存地址,對元素值進行修改時,相互影響
package main import "fmt" func main() { a := [] int{1,2,3,4} b := a[:] // 全切片時Data ptr指向a的第一個元素的內存地址 fmt.Printf("a[0] %p\n",&a[0]) fmt.Printf("b[0] %p\n",&b[0]) // 修改元素值相互影響 b[0] = 10 fmt.Println(a) fmt.Println(b) } //a[0] 0xc000054140 //b[0] 0xc000054140 //[10 2 3 4] //[10 2 3 4]
copy函數進行操作時,會對真實數據進行內存拷貝,新的切片Data指向新的地址
package main import "fmt" func main() { a := [] int{1,2,3,4} b := make([]int, len(a), cap(a)) copy(b,a) // Data ptr指向的新的內存地址 fmt.Printf("a[0] %p\n",&a[0]) fmt.Printf("b[0] %p\n",&b[0]) // 修改元素值相互不影響 b[0] = 10 fmt.Println(a) fmt.Println(b) } //a[0] 0xc000054140 //b[0] 0xc000054160 //[1 2 3 4] //[10 2 3 4]
添加元素
內置的泛型函數append
可以在切片的尾部追加N
個元素:
package main import "fmt" func main() { var a []int // 追加1個元素 a = append(a, 1) // 追加多個元素, 手寫解包方式 a = append(a, 1, 2, 3) // 追加一個切片, 切片需要解包 a = append(a, []int{1,2,3}...) fmt.Println(a) } //[1 1 2 3 1 2 3]
不過要注意的是,在容量不足的情況下,append
的操作會導致重新分配內存,可能導致巨大的內存分配和復制數據代價。即使容量足夠,依然需要用append
函數的返回值來更新切片本身,因為新切片的長度已經發生了變化。
除了在切片的尾部追加,我們還可以在切片的開頭添加元素:
package main import "fmt" func main() { var a = []int{1,2,3} // 在開頭添加1個元素 a = append([]int{0}, a...) // 在開頭添加1個切片 a = append([]int{-3,-2,-1}, a...) fmt.Println(a) } //[-3 -2 -1 0 1 2 3]
在開頭一般都會導致內存的重新分配,而且會導致已有的元素全部復制1次。因此,從切片的開頭添加元素的性能一般要比從尾部追加元素的性能差很多。
由於append
函數返回新的切片,也就是它支持鏈式操作。我們可以將多個append
操作組合起來,實現在切片中間插入元素:
package main import "fmt" func main() { var a []int // 在第i個位置插入x a = append(a[:i], append([]int{x}, a[i:]...)...) // 在第i個位置插入切片 a = append(a[:i], append([]int{1,2,3}, a[i:]...)...) } //[-3 -2 -1 0 1 2 3]
append與內存地址變化
當指向append操作時,會對切片的真實數據進行內存拷貝,與初始切片互不影響
package main import "fmt" func main() { a := [] int{1,2,3,4} b := append(a, 5) // 執行append時 Data ptr指向全新的內存地址 fmt.Printf("a[0] %p\n",&a[0]) fmt.Printf("b[0] %p\n",&b[0]) // 修改元素值相互不影響 b[0] = 10 fmt.Println(a) fmt.Println(b) } //a[0] 0xc000054140 //b[0] 0xc00006e100 //[1 2 3 4] //[10 2 3 4 5]
刪除元素
利用切片和append操作組合進行刪除
package main import "fmt" func main() { a := []int{1, 2, 3, 4, 5} // 刪除尾部1個元素 a = a[:len(a)-1] fmt.Println(a) // 刪除頭部1個元素 a = a[1:len(a)] fmt.Println(a) // 刪除中間一個元素 a = append(a[:1],a[2:]...) fmt.Println(a) } //[1 2 3 4] //[2 3 4] //[2 4]
函數傳參
切片作為參數傳遞是,與賦值一致,只拷貝了結構體中的變量,Data指向的是同一塊地址
package main import "fmt" func change(list []int){ // 拷貝了 Data ptr指向的內存地址 fmt.Printf("list %p\n",&list[0]) // 對切片進行修改 list[0] = 100 } func main() { list := [] int{1,2,3,4} fmt.Printf("list %p\n",&list[0]) change(list) // slice 受影響 fmt.Println(list) } //list 0xc000054140 //list 0xc000054140 //[100 2 3 4]
Cap超出時才會重新內存分配
package main import "fmt" func main() { slice := []int{1,2,3,4} slice1 := slice slice2 := append(slice[:2],5) fmt.Println(slice2) fmt.Println(slice1) fmt.Println(&slice2[2]) fmt.Println(&slice1[2]) } //[1 2 5] //[1 2 5 4] //0xc00000e370 //0xc00000e370
切片與Cap之間的關系
package main import "fmt" func main() { // len = cap a := [] int {1,2,3,4,5} fmt.Printf("len(a)=%d,cap(a)=%d\n",len(a),cap(a)) b := a[1:3] fmt.Println("b =",b) fmt.Printf("len(b)=%d,cap(b)=%d\n",len(b),cap(b)) c := a[:] fmt.Println("c =",c) fmt.Printf("len(c)=%d,cap(c)=%d\n",len(c),cap(c)) d := a[:4] fmt.Println("d =",d) fmt.Printf("len(d)=%d,cap(d)=%d\n",len(d),cap(d)) // len != cap e := d[:2] fmt.Println("e =",e) fmt.Printf("len(e)=%d,cap(e)=%d\n",len(e),cap(e)) f := d[1:3] fmt.Println("f =",f) fmt.Printf("len(f)=%d,cap(f)=%d\n",len(f),cap(f)) } //len(a)=5,cap(a)=5 //b = [2 3] //len(b)=2,cap(b)=4 //c = [1 2 3 4 5] //len(c)=5,cap(c)=5 //d = [1 2 3 4] //len(d)=4,cap(d)=5 //e = [1 2] //len(e)=2,cap(e)=5 //f = [2 3] //len(f)=2,cap(f)=4