Golang學習筆記--Array和Slice
目錄
Reference
https://blog.golang.org/go-slices-usage-and-internals
Array
- 陣列是值型別,賦值和傳參會複製整個陣列,而不是指標。
- 陣列⻓長度必須是常量,且是型別的組成部分。[2]int 和 [3]int 是不同型別。
- ⽀支援 "=="、"!=" 操作符,因為記憶體總是被初始化過的。
- 指標陣列 [n]*T,陣列指標 *[n]T。
Slice
因為array是值型別,賦值和傳參行為使用array都會發生資料拷貝,如果操作物件是比較大的array,那麼時間和空間上的開銷都會成為問題。
7 func arrayAsParamTest() { 8 fmt.Println("=========SliceTest.sliceAsParamTest()==========") 9 arrayA := [2]int{1, 2} 10 var arrayB [2]int 11 arrayB = arrayA 12 fmt.Printf("Address: %p.\n", &arrayA) 13 fmt.Printf("Address: %p.\n", &arrayB) 14 arrayAsParam(arrayA) 15 }
=========SliceTest.sliceAsParamTest()==========
Address: 0xc420014390.
Address: 0xc4200143a0.
Address: 0xc4200143b0.
傳參時用陣列指標可以避免資料拷貝,但是如果原陣列指標指向了新的陣列,那麼函式裡面的指標也會隨之變化,很容易引入bug。
slice是建立在陣列基礎之上的一種資料型別。slice本身只儲存元資料,使用者資料儲存在底層array,slice用一個指標指向底層陣列(可以指向底層陣列任意元素,指向的元素也就是slice的首元素),並用length限定slice的讀寫區域,用capacity表示slice的容量,capacity減去length就是slice的剩餘儲存空間。
slice的建立
建立slice有以下幾種用法:
1.通過make函式。make([]type, length, capacity),make函式在堆記憶體上分配底層陣列。
2.用[]對array或者slice執行切片操作得到一個新的slice。
3.用字面值初始化一個slice。
4.建立一個nil slice,它的值就等於nil。
func slice() {
fmt.Println("=========SliceTest.slice()========")
//1
a := make([]int, 5) //len = 5, cap = 5
b := make([]int, 0, 5) //len = 0, cap = 5
b1 := make([]int, 0) //empty slice
//2
c := b[:2] //len = 2 , cap = 5
d := c[2:5] //len = 3, cap = 3
//3
e := [3]bool{true, false, true}
e1 := []bool{} //empty slice
//4, nil slice
var f []int
}
上面demo中有兩種特殊的slice,empty slice和nil slice,其區別在於:empty slice指向的地址不是nil,指向的是一個記憶體地址,但是它沒有分配任何記憶體空間,即底層元素包含0個元素。函式發生異常,需要返回slice,這種情況通常返回的是nil slice。如果是查詢函式,沒有查詢到結果,那麼返回的通常是empty slice。
因此,在處理這類函式呼叫的結果時,經常先通過nil(if result == nil)判斷函式執行是否出錯,返回結果不等於nil再進行處理。
Slice常用操作
reslice
reslice其實就是在現有slice物件的基礎上進行切片來建立新的slice物件,以便在capacity允許的範圍內調整屬性,並且共享底層陣列。
append函式
我們可以通過append函式向slice尾部新增新的元素,有兩種基本情況:
1.新增元素後slice的length不超出capacity,將對原底層陣列的資料進行修改,如果該陣列有多個切片,需要注意這種相互影響。
2.新增元素後slice的length超出capacity,將會申請新的底層陣列並拷貝資料。
函式原型如下:
func append(s []T, vs ...T) []T
func printSlice(s string, x []int) {
fmt.Printf("%s len=%d cap=%d adress=%p value=%v\n", s, len(x), cap(x), &x, x)
}
func SliceAppend() {
fmt.Println("**********Test - Test append*********")
var s []int
printSlice("s", s)
// append works on nil slices.
s = append(s, 0)
printSlice("s", s)
// The slice grows as needed.
s = append(s, 1)
printSlice("s", s)
// We can add more than one element at a time.
s = append(s, 2)
printSlice("s", s)
s = append(s, 3, 4)
printSlice("s", s)
s = append(s, 5, 6)
printSlice("s", s)
}
**********Test - Test append*********
s len=0 cap=0 adress=0xc00000a320 value=[]
s len=1 cap=1 adress=0xc00000a340 value=[0]
s len=2 cap=2 adress=0xc00000a380 value=[0 1]
s len=3 cap=4 adress=0xc00000a3c0 value=[0 1 2]
s len=5 cap=8 adress=0xc00000a400 value=[0 1 2 3 4]
s len=7 cap=8 adress=0xc00000a440 value=[0 1 2 3 4 5 6]
從上面的demo中我們可以發現,用append函式向slice壓入新的元素時,如果slice底層的陣列空間不足,將會分配一個新的陣列,然後讓slice的指標指向這個新的陣列,capacity也修改為新陣列的長度。看起來每次重新分配陣列的時候,在滿足slice的長度需求的同時,size都是之前的2倍,也就是會預留一部分空間。但是,下面我們稍微修改一下SliceAppend函式,value分別為2,3,4的三個元素一次append到slice:
func SliceAppend() {
fmt.Println("**********Test - Test append*********")
var s []int
printSlice("s", s)
// append works on nil slices.
s = append(s, 0)
printSlice("s", s)
// The slice grows as needed.
s = append(s, 1)
printSlice("s", s)
// We can add more than one element at a time.
s = append(s, 2, 3, 4)
printSlice("s", s)
s = append(s, 5, 6)
printSlice("s", s)
}
**********Test - Test append*********
s len=0 cap=0 adress=0xc00000a320 value=[]
s len=1 cap=1 adress=0xc00000a340 value=[0]
s len=2 cap=2 adress=0xc00000a380 value=[0 1]
s len=5 cap=6 adress=0xc00000a3c0 value=[0 1 2 3 4]
s len=7 cap=12 adress=0xc00000a400 value=[0 1 2 3 4 5 6]
在slice的元素個數為5時,重新分配的陣列的size不再是8,而是6了;slice元素個數為7時,cap不再是8,而是12。相比第一份程式碼,僅僅是將中間兩次append操作合併到一次而已,看來底層陣列的分配演算法並沒有我們前面想象的那麼簡單。
那麼在slice擴容時,底層陣列的size到底是怎樣去分配的呢?擴容策略如下:
1.如果新的size超出現有size的2倍,分配的大小就是新的size,如果新size是奇數還會加1(size為1是例外)。
2.否則;如果當前size小於1024,按每次2倍size分配空間,否則每次按當前size的四分之一擴容。
copy函式
30 func sliceCopy() {
31 fmt.Println("=========SliceTest.sliceCopy()==========")
32 s := []string{"hunk", "jack", "bob"}
33 d := make([]string, 3, 3)
34 copy(d, s)
35 fmt.Printf("s len=%d cap=%d adress=%p value=%v\n", len(s), cap(s), &s, s)
36 fmt.Printf("d len=%d cap=%d adress=%p value=%v\n", len(d), cap(d), &d, d)
37 for i, v := range d {
38 fmt.Printf("d[%d] is %s\n", i, v)
39 }
40 }
=========SliceTest.sliceCopy()==========
s len=3 cap=3 adress=0xc42000a440 value=[hunk jack bob]
d len=3 cap=3 adress=0xc42000a460 value=[hunk jack bob]
d[0] is hunk
d[1] is jack
d[2] is bob
copy函式會返回實際拷貝的元素個數,這也取決於源和目標兩者長度較短的那一個。
range遍歷
slice既然也是一組資料的集合,必然支援range對其進行遍歷。
46 /*
47 When ranging over a slice, two values are returned for each iteration. The first is the index, and the second is a copy of the element at that index.
48 */
49 func sliceRange() {
50 fmt.Println("=========SliceTest.sliceRange()=========")
51 var pow = []int{1, 2, 4, 8}
52
53 for i, v := range pow {
54 fmt.Printf("value = %d, value.address = %x, slice-address = %x.\n", v, &v, &pow[i])
55 }
56
57 for i := range pow {
58 fmt.Printf("%d\n", i)
59 }
60
61 for _, v := range pow {
62 fmt.Printf("%d\n", v)
63 }
64 }
=========SliceTest.sliceRange()=========
value = 1, value.address = c4200142e8, slice-address = c4200120c0.
value = 2, value.address = c4200142e8, slice-address = c4200120c8.
value = 4, value.address = c4200142e8, slice-address = c4200120d0.
value = 8, value.address = c4200142e8, slice-address = c4200120d8.
用range對slice進行遍歷時,每次返回index和value兩個資料,可以根據需要省略取值。需要注意的是,value是值拷貝的結果,並且每次的value都用了同一塊儲存區域,所以range遍歷時修改返回的value無法修改到底層陣列的資料,可以用slice[index]對其修改。