文章來源:https://blog.thinkeridea.com/...html
array
和 slice
看似類似,卻有着極大的不一樣,但他們之間還有着千次萬縷的聯繫 slice
是引用類型、是 array
的引用,至關於動態數組,
這些都是 slice
的特性,可是 slice
底層如何表現,內存中是如何分配的,特別是在程序中大量使用 slice
的狀況下,怎樣能夠高效使用 slice
?
今天藉助 Go
的 unsafe
包來探索 array
和 slice
的各類奧妙。linux
slice
是在 array
的基礎上實現的,須要先詳細瞭解一下數組。git
維基上如此介紹數組:github
在計算機科學中,數組數據結構(英語:array data structure),簡稱數組(英語:Array),是由相同類型的元素(element)的集合所組成的數據結構,分配一塊連續的內存來存儲,利用元素的索引(index)能夠計算出該元素對應的存儲地址。
數組設計之初是在形式上依賴內存分配而成的,因此必須在使用前預先請求空間。這使得數組有如下特性:golang
- 請求空間之後大小固定,不能再改變(數據溢出問題);
- 在內存中有空間連續性的表現,中間不會存在其餘程序須要調用的數據,爲此數組的專用內存空間;
- 在舊式編程語言中(若有中階語言之稱的C),程序不會對數組的操做作下界判斷,也就有潛在的越界操做的風險(好比會把數據寫在運行中程序須要調用的核心部分的內存上)。
根據維基的介紹,瞭解到數組是存儲在一段連續的內存中,每一個元素的類型相同,便是每一個元素的寬度相同,能夠根據元素的寬度計算元素存儲的位置。
經過這段介紹總結一下數組有一下特性:算法
Go
中的數組如何實現的呢,偏偏就是這麼實現的,實際上幾乎全部計算機語言,數組的實現都是類似的,也擁有上面總結的特性。Go
語言的數組不一樣於 C
語言或者其餘語言的數組,C
語言的數組變量是指向數組第一個元素的指針;
而 Go
語言的數組是一個值,Go
語言中的數組是值類型,一個數組變量就表示着整個數組,意味着 Go
語言的數組在傳遞的時候,傳遞的是原數組的拷貝。編程
在程序中數組的初始化有兩種方法 arr := [10]int{}
或 var arr [10]int
,可是不能使用 make
來建立,數組這節結束時再探討一下這個問題。
使用 unsafe
來看一下在內存中都是如何存儲的吧:json
package main import ( "fmt" "unsafe" ) func main() { var arr = [3]int{1, 2, 3} fmt.Println(unsafe.Sizeof(arr)) size := unsafe.Sizeof(arr[0]) // 獲取數組指定索引元素的值 fmt.Println(*(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&arr[0])) + 1*size))) // 設置數組指定索引元素的值 *(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&arr[0])) + 1*size)) = 10 fmt.Println(arr[1]) }
這段代碼的輸出以下 (Go Playground):數組
12
2
10
首先說 12
是 fmt.Println(unsafe.Sizeof(arr))
輸出的,unsafe.Sizeof
用來計算當前變量的值在內存中的大小,12
這個表明一個 int
有4個字節,3 * 4
就是 12
。
這是在32位平臺上運行得出的結果, 若是在64位平臺上運行數組的大小是 24
。從這裏能夠看出 [3]int
在內存中由3個連續的 int
類型組成,且有 12
個字節那麼長,這就說明了數組在內存中沒有存儲多餘的數據,只存儲元素自己。數據結構
size := unsafe.Sizeof(arr[0])
用來計算單個元素的寬度,int
在32位平臺上就是4個字節,uintptr(unsafe.Pointer(&arr[0]))
用來計算數組起始位置的指針,1*size
用來獲取索引爲1的元素相對數組起始位置的偏移,unsafe.Pointer(uintptr(unsafe.Pointer(&arr[0])) + 1*size))
獲取索引爲1的元素指針,*(*int)
用來轉換指針位置的數據類型, 由於 int
是4個字節,因此只會讀取4個字節的數據,由元素類型限制數據寬度,來肯定元素的結束位置,所以獲得的結果是 2
。
上一個步驟獲取元素的值,其中先獲取了元素的指針,賦值的時候只須要對這個指針位置設置值就能夠了, *(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&arr[0])) + 1*size)) = 10
就是用來給指定下標元素賦值。
package main import ( "fmt" "unsafe" ) func main() { n:= 10 var arr = [n]int{} fmt.Println(arr) }
如上代碼,動態的給數組設定長度,會致使編譯錯誤 non-constant array bound n
, 由此推導數組的全部操做都是編譯時完成的,會轉成對應的指令,經過這個特性知道數組的長度是數組類型不可或缺的一部分,而且必須在編寫程序時肯定。
能夠經過 GOOS=linux GOARCH=amd64 go tool compile -S array.go
來獲取對應的彙編代碼,在 array.go
中作一些數組相關的操做,查看轉換對應的指令。
以前的疑問,爲何數組不能用 make
建立? 上面分析瞭解到數組操做是在編譯時轉換成對應指令的,而 make
是在運行時處理(特殊狀態下會作編譯器優化,make能夠被優化,下面 slice
分析時來說)。
由於數組是固定長度且是值傳遞,很不靈活,因此在 Go
程序中不多看到數組的影子。然而 slice
無處不在,slice
以數組爲基礎,提供強大的功能和遍歷性。slice
的類型規範是[]T,slice
T元素的類型。與數組類型不一樣,slice
類型沒有指定的長度。
slice
申明的幾種方法:
s := []int{1, 2, 3}
簡短的賦值語句
var s []int
var
申明
make([]int, 3, 8)
或make([]int, 3)
make
內置方法建立
s := ss[:5]
從切片或者數組建立
slice
有兩個內置函數來獲取其屬性:
len
獲取slice
的長度
cap
獲取slice
的容量
slice
的屬性,這東西是什麼,還需藉助 unsafe
來探究一下。
package main import ( "fmt" "unsafe" ) func main() { s := make([]int, 10, 20) s[2] = 100 s[9] = 200 size := unsafe.Sizeof(0) fmt.Printf("%x\n", *(*uintptr)(unsafe.Pointer(&s))) fmt.Println(*(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&s)) + size))) fmt.Println(*(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&s)) + size*2))) fmt.Println(*(*[20]int)(unsafe.Pointer(*(*uintptr)(unsafe.Pointer(&s))))) }
這段代碼的輸出以下 (Go Playground):
c00007ce90
10
20
[0 0 100 0 0 0 0 0 0 200 0 0 0 0 0 0 0 0 0 0]
這段輸出除了第一個,剩餘三個好像都能看出點什麼, 10
不是建立 slice
的長度嗎,20
不就是指定的容量嗎, 最後這個看起來有點像 slice
裏面的數據,可是數量貌似有點多,從第三個元素和第十個元素來看,正好是給 slice
索引 2
和 10
指定的值,可是切片不是長度是 10
個嗎,難道這個是容量,容量恰好是 20
個。
第二和第三個輸出很好弄明白,就是 slice
的長度和容量, 最後一個實際上是 slice
引用底層數組的數據,由於建立容量爲 20
,因此底層數組的長度就是 20
,從這裏瞭解到切片是引用底層數組上的一段數據,底層數組的長度就是 slice
的容量,因爲數組長度不可變的特性,當 slice
的長度達到容量大小以後就須要考慮擴容,不是說數組長度不能變嗎,那 slice
怎麼實現擴容呢, 其實就是在內存上分配一個更大的數組,把當前數組上的內容拷貝到新的數組上, slice
來引用新的數組,這樣就實現擴容了。
說了這麼多,仍是沒有看出來 slice
是如何引用數組的,額…… 以前的程序還有一個輸出沒有搞懂是什麼,難道這個就是底層數組的引用。
package main import ( "fmt" "unsafe" ) func main() { arr := [10]int{1, 2, 3} arr[7] = 100 arr[9] = 200 fmt.Println(arr) s1 := arr[:] s2 := arr[2:8] size := unsafe.Sizeof(0) fmt.Println("----------s1---------") fmt.Printf("%x\n", *(*uintptr)(unsafe.Pointer(&s1))) fmt.Printf("%x\n", uintptr(unsafe.Pointer(&arr[0]))) fmt.Println(*(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&s1)) + size))) fmt.Println(*(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&s1)) + size*2))) fmt.Println(s1) fmt.Println(*(*[10]int)(unsafe.Pointer(*(*uintptr)(unsafe.Pointer(&s1))))) fmt.Println("----------s2---------") fmt.Printf("%x\n", *(*uintptr)(unsafe.Pointer(&s2))) fmt.Printf("%x\n", uintptr(unsafe.Pointer(&arr[0]))+size*2) fmt.Println(*(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&s2)) + size))) fmt.Println(*(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&s2)) + size*2))) fmt.Println(s2) fmt.Println(*(*[8]int)(unsafe.Pointer(*(*uintptr)(unsafe.Pointer(&s2))))) }
以上代碼輸出以下(Go Playground):
[1 2 3 0 0 0 0 100 0 200]
----------s1---------
c00001c0a0
c00001c0a0
10
10
[1 2 3 0 0 0 0 100 0 200]
[1 2 3 0 0 0 0 100 0 200]
----------s2---------
c00001c0b0
c00001c0b0
6
8
[3 0 0 0 0 100]
[3 0 0 0 0 100 0 200]
這段輸出看起來有點小複雜,第一行輸出就不用說了吧,這個是打印整個數組的數據。先分析一下 s1
變量的下面的輸出吧,s1 := arr[:]
引用了整個數組,因此在第五、6行輸出都是10,由於數組長度爲10,全部 s1
的長度和容量都爲10,那第三、4行輸出是什麼呢,他們怎麼都同樣呢,以前分析數組的時候 經過 uintptr(unsafe.Pointer(&arr[0]))
來獲取數組起始位置的指針的,那麼第4行打印的就是數組的指針,這麼就瞭解了第三行輸出的是上面了吧,就是數組起始位置的指針,因此 *(*uintptr)(unsafe.Pointer(&s1))
獲取的就是引用數組的指針,可是這個並非數組起始位置的指針,而是 slice
引用數組元素的指針,爲何這麼說呢?
接着看 s2
變量下面的輸出吧,s2 := arr[2:8]
引用數組第3~8的元素,那麼 s2
的長度就是 6。 根據經驗能夠知道 s2
變量輸出下面第3行就是 slice
的長度,可是爲啥第4行是 8
呢,slice
應用數組的指定索引發始位置到數組結尾就是 slice
的容量, 因此 因此從第3個位置到末尾,就是8個容量。在看第1行和第2行的輸出,以前分析數組的時候經過 uintptr(unsafe.Pointer(&arr[0]))+size*2
來獲取數組指定索引位置的指針,那麼這段第2行就是數組索引爲2的元素指針,*(*uintptr)(unsafe.Pointer(&s2))
是獲取切片的指針,第1行和第2行輸出一致,因此 slice
實際是引用數組元素位置的指針,並非數組起始位置的指針。
總結:
slice
是的起始位置是引用數組元素位置的指針。slice
的長度是引用數組元素起始位置到結束位置的長度。slice
的容量是引用數組元素起始位置到數組末尾的長度。通過上面一輪分析瞭解到 slice
有三個屬性,引用數組元素位置指針、長度和容量。實際上 slice
的結構像下圖同樣:
slice
是如何增加的,用 unsafe
分析一下看看:
package main import ( "fmt" "unsafe" ) func main() { s := make([]int, 9, 10) // 引用底層的數組地址 fmt.Printf("%x\n", *(*uintptr)(unsafe.Pointer(&s))) s = append(s, 1) // 引用底層的數組地址 fmt.Printf("%x\n", *(*uintptr)(unsafe.Pointer(&s))) s = append(s, 1) // 引用底層的數組地址 fmt.Printf("%x\n", *(*uintptr)(unsafe.Pointer(&s))) }
以上代碼的輸出(Go Playground):
c000082e90
9 10
c000082e90
10 10
c00009a000
11 20
從結果上看前兩次地址是同樣的,初始化一個長度爲9,容量爲10的 slice
,當第一次 append
的時候容量是足夠的,因此底層引用數組地址未發生變化,此時 slice
的長度和容量都爲10,以後再次 append
的時候發現底層數組的地址不同了,由於 slice
的長度超過了容量,可是新的 slice
容量並非11而是20,這要說 slice
的機制了,由於數組長度不可變,想擴容 slice
就必須分配一個更大的數組,並把以前的數據拷貝到新數組,若是一次只增長1個長度,那就會那發生大量的內存分配和數據拷貝,這個成本是很大的,因此 slice
是有一個增加策略的。
Go
標準庫 runtime/slice.go
當中有詳細的 slice
增加策略的邏輯:
func growslice(et *_type, old slice, cap int) slice { ..... // 計算新的容量,核心算法用來決定slice容量增加 newcap := old.cap doublecap := newcap + newcap if cap > doublecap { newcap = cap } else { if old.len < 1024 { newcap = doublecap } else { for 0 < newcap && newcap < cap { newcap += newcap / 4 } if newcap <= 0 { newcap = cap } } } // 根據et.size調整新的容量 var overflow bool var lenmem, newlenmem, capmem uintptr switch { case et.size == 1: lenmem = uintptr(old.len) newlenmem = uintptr(cap) capmem = roundupsize(uintptr(newcap)) overflow = uintptr(newcap) > maxAlloc newcap = int(capmem) case et.size == sys.PtrSize: lenmem = uintptr(old.len) * sys.PtrSize newlenmem = uintptr(cap) * sys.PtrSize capmem = roundupsize(uintptr(newcap) * sys.PtrSize) overflow = uintptr(newcap) > maxAlloc/sys.PtrSize newcap = int(capmem / sys.PtrSize) case isPowerOfTwo(et.size): var shift uintptr if sys.PtrSize == 8 { // Mask shift for better code generation. shift = uintptr(sys.Ctz64(uint64(et.size))) & 63 } else { shift = uintptr(sys.Ctz32(uint32(et.size))) & 31 } lenmem = uintptr(old.len) << shift newlenmem = uintptr(cap) << shift capmem = roundupsize(uintptr(newcap) << shift) overflow = uintptr(newcap) > (maxAlloc >> shift) newcap = int(capmem >> shift) default: lenmem = uintptr(old.len) * et.size newlenmem = uintptr(cap) * et.size capmem = roundupsize(uintptr(newcap) * et.size) overflow = uintptr(newcap) > maxSliceCap(et.size) newcap = int(capmem / et.size) } ...... var p unsafe.Pointer if et.kind&kindNoPointers != 0 { p = mallocgc(capmem, nil, false) // 分配新的內存 memmove(p, old.array, lenmem) // 拷貝數據 memclrNoHeapPointers(add(p, newlenmem), capmem-newlenmem) } else { p = mallocgc(capmem, et, true) // 分配新的內存 if !writeBarrier.enabled { memmove(p, old.array, lenmem) } else { for i := uintptr(0); i < lenmem; i += et.size { typedmemmove(et, add(p, i), add(old.array, i)) // 拷貝數據 } } } return slice{p, old.len, newcap} // 新slice引用新的數組,長度爲舊數組的長度,容量爲新數組的容量 }
基本呢就三個步驟,計算新的容量、分配新的數組、拷貝數據到新數組,社區不少人分享 slice
的增加方法,實際都不是很精確,由於你們只分析了計算 newcap
的那一段,也就是上面註釋的第一部分,下面的 switch
根據 et.size
來調整 newcap
一段被直接忽略,社區的結論是:"若是 selic
的容量小於1024個元素,那麼擴容的時候 slice
的 cap
就翻番,乘以2;一旦元素個數超過1024個元素,增加因子就變成1.25,即每次增長原來容量的四分之一" 大多數狀況也確實如此,可是根據 newcap
的計算規則,若是新的容量超過舊的容量2倍時會直接按新的容量分配,真的是這樣嗎?
package main import ( "fmt" ) func main() { s := make([]int, 10, 10) fmt.Println(len(s), cap(s)) s2 := make([]int, 40) s = append(s, s2...) fmt.Println(len(s), cap(s)) }
以上代碼的輸出(Go Playground):
10 10
50 52
這個結果有點出人意料, 若是是2倍增加應該是 10 * 2 * 2 * 2
結果應該是80, 若是說新的容量高於舊容量的兩倍但結果也不是50,實際上 newcap
的結果就是50,那段邏輯很好理解,可是switch
根據 et.size
來調整 newcap
後就是52了,這段邏輯走到了 case et.size == sys.PtrSize
這段,詳細的之後作源碼分析再說。
總結
slice
的長度超過其容量,會分配新的數組,並把舊數組上的值拷貝到新的數組slice
並操過其容量, 若是 selic
的容量小於1024個元素,那麼擴容的時候 slice
的 cap
就翻番,乘以2;一旦元素個數超過1024個元素,增加因子就變成1.25,即每次增長原來容量的四分之一。slice
發生擴容,引用新數組後,slice
操做不會再影響舊的數組,而是新的數組(社區常常討論的傳遞 slice
容量超出後,修改數據不會做用到舊的數據上),因此每每設計函數若是會對長度調整都會返回新的 slice
,例如 append
方法。slice
不發生擴容,全部的修改都會做用在原數組上,那若是把 slice
傳遞給一個函數或者賦值給另外一個變量會發生什麼呢,slice
是引用類型,會有新的內存被分配嗎。
package main import ( "fmt" "strings" "unsafe" ) func main() { s := make([]int, 10, 20) size := unsafe.Sizeof(0) fmt.Printf("%p\n", &s) fmt.Printf("%x\n", *(*uintptr)(unsafe.Pointer(&s))) fmt.Println(*(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&s)) + size))) fmt.Println(*(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&s)) + size*2))) slice(s) s1 := s fmt.Printf("%p\n", &s1) fmt.Printf("%x\n", *(*uintptr)(unsafe.Pointer(&s1))) fmt.Println(*(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&s1)) + size))) fmt.Println(*(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&s1)) + size*2))) fmt.Println(strings.Repeat("-", 50)) *(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&s1)) + size)) = 20 fmt.Printf("%x\n", *(*uintptr)(unsafe.Pointer(&s))) fmt.Println(*(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&s)) + size))) fmt.Println(*(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&s)) + size*2))) fmt.Printf("%x\n", *(*uintptr)(unsafe.Pointer(&s1))) fmt.Println(*(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&s1)) + size))) fmt.Println(*(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&s1)) + size*2))) fmt.Println(s) fmt.Println(s1) fmt.Println(strings.Repeat("-", 50)) s2 := s s2 = append(s2, 1) fmt.Println(len(s), cap(s), s) fmt.Println(len(s1), cap(s1), s1) fmt.Println(len(s2), cap(s2), s2) } func slice(s []int) { size := unsafe.Sizeof(0) fmt.Printf("%p\n", &s) fmt.Printf("%x\n", *(*uintptr)(unsafe.Pointer(&s))) fmt.Println(*(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&s)) + size))) fmt.Println(*(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&s)) + size*2))) }
這個例子(Go Playground)比較長就不逐一分析了,在這個例子裏面調用函數傳遞 slice
其變量的地址發生了變化, 可是引用數組的地址,slice
的長度和容量都沒有變化, 這說明是對 slice
的淺拷貝,拷貝 slice
的三個屬性建立一個新的變量,雖然引用底層數組仍是一個,可是變量並非一個。
第二個建立 s1
變量,使用 s
爲其賦值,發現 s1
和函數調用同樣也是 s
的淺拷貝,以後修改 s1
的長度發現 s1
的長度發生變化,可是 s
的長度保持不變, 這也說明 s1
就是 s
的淺拷貝。
這樣設計有什麼優點呢,第三步建立 s2
變量, 而且 append
一個元素, 發現 s2
的長度發生變化了, s
並無,雖然這個數據就在底層數組上,可是用常規的方法 s
是看不到第11個位置上的數據的, s1
由於長度覆蓋到第11個元素,全部可以看到這個數據的變化。這裏能看到採用淺拷貝的方式可使得切片的屬性各自獨立,而不會相互影響,這樣能夠有必定的隔離性,缺點也很明顯,若是兩個變量都引用同一個數組,同時 append
, 在不發生擴容的狀況下,老是最後一個 append
的結果被保留,可能引發一些編程上疑惑。
總結
slice
是引用類型,可是和 C
傳引用是有區別的, C
裏面的傳引用是在編譯器對原變量數據引用, 並不會發生內存分配,而 Go
裏面的引用類型傳遞和賦值會進行淺拷貝,在32位平臺上有12個字節的內存分配, 在64位上有24字節的內存分配。
傳引用和引用類型是有區別的, slice
是引用類型。
slice
有三種狀態:零切片、空切片、nil切片。
全部的類型都有零值,若是 slice
所引用數組元素都沒有賦值,就是全部元素都是類型零值,那這就是零切片。
package main import "fmt" func main() { var s = make([]int, 10) fmt.Println(s) var s1 = make([]*int, 10) fmt.Println(s1) var s2 = make([]string, 10) fmt.Println(s2) }
以上代碼輸出(Go Playground):
[0 0 0 0 0 0 0 0 0 0]
[<nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil>]
[ ]
零切片很好理解,數組元素都爲類型零值即爲零切片,這種狀態下的 slice
和正常的 slice
操做沒有任何區別。
空切片能夠理解就是切片的長度爲0,就是說 slice
沒有元素。 社區大多數解釋空切片爲引用底層數組爲 zerobase
這個特殊的指針。可是從操做上看空切片全部的表現就是切片長度爲0,若是容量也爲零底層數組就會指向 zerobase
,這樣就不會發生內存分配, 若是容量不會零就會指向底層數據,會有內存分配。
package main import ( "fmt" "reflect" "strings" "unsafe" ) func main() { var s []int s1 := make([]int, 0) s2 := make([]int, 0, 0) s3 := make([]int, 0, 100) arr := [10]int{} s4 := arr[:0] fmt.Println(strings.Repeat("--s--", 10)) fmt.Println(*(*reflect.SliceHeader)(unsafe.Pointer(&s))) fmt.Println(s) fmt.Println(s == nil) fmt.Println(strings.Repeat("--s1--", 10)) fmt.Println(*(*reflect.SliceHeader)(unsafe.Pointer(&s1))) fmt.Println(s1) fmt.Println(s1 == nil) fmt.Println(strings.Repeat("--s2--", 10)) fmt.Println(*(*reflect.SliceHeader)(unsafe.Pointer(&s2))) fmt.Println(s2) fmt.Println(s2 == nil) fmt.Println(strings.Repeat("--s3--", 10)) fmt.Println(*(*reflect.SliceHeader)(unsafe.Pointer(&s3))) fmt.Println(s3) fmt.Println(s3 == nil) fmt.Println(strings.Repeat("--s4--", 10)) fmt.Println(*(*reflect.SliceHeader)(unsafe.Pointer(&s4))) fmt.Println(s4) fmt.Println(s4 == nil) }
以上代碼輸出(Go Playground):
--s----s----s----s----s----s----s----s----s----s--
{0 0 0}
[]
--s1----s1----s1----s1----s1----s1----s1----s1----s1----s1--
{18349960 0 0}
[]
--s2----s2----s2----s2----s2----s2----s2----s2----s2----s2--
{18349960 0 0}
[]
--s3----s3----s3----s3----s3----s3----s3----s3----s3----s3--
{824634269696 0 100}
[]
--s4----s4----s4----s4----s4----s4----s4----s4----s4----s4--
{824633835680 0 10}
[]
以上示例中除了 s
其它的 slice
都是空切片,打印出來所有都是 []
,s
是nil切片下一小節說。要注意 s1
和 s2
的長度和容量都爲0,且引用數組指針都是 18349960
, 這點過重要了,由於他們都指向 zerobase
這個特殊的指針,是沒有內存分配的。
什麼是nil切片,這個名字說明nil切片沒有引用任何底層數組,底層數組的地址爲nil就是nil切片。上一小節中的 s
就是一個nil切片,它的底層數組指針爲0,表明是一個 nil
指針。
零切片就是其元素值都是元素類型的零值的切片。
空切片就是數組指針不爲nil,且 slice
的長度爲0。
nil切片就是引用底層數組指針爲 nil
的 slice
。
操做上零切片、空切片和正常的切片都沒有任何區別,可是nil切片會多兩個特性,一個nil切片等於 nil
值,且進行 json
序列化時其值爲 null
,nil切片還能夠經過賦值爲 nil
得到。
對數組和 slice
作了性能測試,源碼在 GitHub。
對不一樣容量和數組和切片作性能測試,代碼以下,分爲:100、1000、10000、100000、1000000、10000000
func BenchmarkSlice100(b *testing.B) { for i := 0; i < b.N; i++ { s := make([]int, 100) for i, v := range s { s[i] = 1 + i _ = v } } } func BenchmarkArray100(b *testing.B) { for i := 0; i < b.N; i++ { a := [100]int{} for i, v := range a { a[i] = 1 + i _ = v } } }
測試結果以下:
goos: darwin
goarch: amd64
pkg: github.com/thinkeridea/example/array_slice/test
BenchmarkSlice100-8 20000000 69.8 ns/op 0 B/op 0 allocs/op
BenchmarkArray100-8 20000000 69.0 ns/op 0 B/op 0 allocs/op
BenchmarkSlice1000-8 5000000 318 ns/op 0 B/op 0 allocs/op
BenchmarkArray1000-8 5000000 316 ns/op 0 B/op 0 allocs/op
BenchmarkSlice10000-8 200000 9024 ns/op 81920 B/op 1 allocs/op
BenchmarkArray10000-8 500000 3143 ns/op 0 B/op 0 allocs/op
BenchmarkSlice100000-8 10000 114398 ns/op 802816 B/op 1 allocs/op
BenchmarkArray100000-8 20000 61856 ns/op 0 B/op 0 allocs/op
BenchmarkSlice1000000-8 2000 927946 ns/op 8003584 B/op 1 allocs/op
BenchmarkArray1000000-8 5000 342442 ns/op 0 B/op 0 allocs/op
BenchmarkSlice10000000-8 100 10555770 ns/op 80003072 B/op 1 allocs/op
BenchmarkArray10000000-8 50 22918998 ns/op 80003072 B/op 1 allocs/op
PASS
ok github.com/thinkeridea/example/array_slice/test 23.333s
從上面的結果能夠發現數組和 slice
在1000之內的容量上時性能機會一致,並且都沒有內存分配,這應該是編譯器對 slice
的特殊優化。
從10000~1000000容量時數組的效率就比slice
好了一倍有餘,主要緣由是數組在沒有內存分配作了編譯優化,而 slice
有內存分配。
可是10000000容量日後數組性能大幅度降低,slice
是數組性能的兩倍,兩個都在運行時作了內存分配,其實這麼大的數組還真是不常見,也沒有比較作編譯器優化了。
slice
和數組有些差異,特別是應用層上,特性差異很大,那什麼時間使用數組,什麼時間使用切片呢。
以前作了性能測試,在1000之內性能幾乎一致,只有10000~1000000時纔會出現數組性能好於 slice
,因爲數組在編譯時肯定長度,也就是再編寫程序時必須確認長度,全部往常不會用到更大的數組,大多數都在1000之內的長度。我認爲若是在編寫程序是就已經肯定數據長度,建議用數組,並且竟多是局部使用的位置建議用數組(避免傳遞產生值拷貝),好比一天24小時,一小時60分鐘,ip是4個 byte
這種狀況是能夠用時數組的。
爲何推薦用數組,只要能在編寫程序是肯定數據長度我都會用數組,由於其類型會幫助閱讀理解程序,dayHour := [24]Data
一眼就知道是按小時切分數據存儲的,如要傳遞數組時能夠考慮傳遞數組的指針,固然會帶來一些操做不方便,往常我使用數組都是不須要傳遞給其它函數的,可能會在 struct
裏面保存數組,而後傳遞 struct
的指針,或者用 unsafe
來反解析數組指針到新的數組,也不會產生數據拷貝,而且只增長一句轉換語句。slice
會比數組多存儲三個 int
的屬性,並且指針引用會增長 GC
掃描的成本,每次傳遞都會對這三個屬性進行拷貝,若是能夠也能夠考慮傳遞 slice
的指針,指針只有一個 int
的大小。
對於不肯定大小的數據只能用 slice
,不然就要本身作擴容很麻煩, 對於肯定大小的集合建議使用數組。
本文做者: 戚銀(thinkeridea)
本文連接: https://blog.thinkeridea.com/...