原文地址:深刻理解 Go Slicegit
在 Go 中,Slice(切片)是抽象在 Array(數組)之上的特殊類型。爲了更好地瞭解 Slice,第一步須要先對 Array 進行理解。深入瞭解 Slice 與 Array 之間的區別後,就能更好的對其底層一番摸索 😄github
func main() { nums := [3]int{} nums[0] = 1 n := nums[0] n = 2 fmt.Printf("nums: %v\n", nums) fmt.Printf("n: %d\n", n) }
咱們可得知在 Go 中,數組類型須要指定長度和元素類型。在上述代碼中,可得知 [3]int{}
表示 3 個整數的數組,並進行了初始化。底層數據存儲爲一段連續的內存空間,經過固定的索引值(下標)進行檢索golang
數組在聲明後,其元素的初始值(也就是零值)爲 0。而且該變量能夠直接使用,不須要特殊操做數組
同時數組的長度是固定的,它的長度是類型的一部分,所以 [3]int
和 [4]int
在類型上是不一樣的,不能稱爲 「一個東西」數據結構
nums: [1 0 0] n: 2
func main() { nums := [3]int{} nums[0] = 1 dnums := nums[:] fmt.Printf("dnums: %v", dnums) }
Slice 是對 Array 的抽象,類型爲 []T
。在上述代碼中,dnums
變量經過 nums[:]
進行賦值。須要注意的是,Slice 和 Array 不同,它不須要指定長度。也更加的靈活,可以自動擴容app
type slice struct { array unsafe.Pointer len int cap int }
Slice 的底層數據結構共分爲三部分,以下:函數
unsafe.Pointer
能夠表示任何可尋址的值的指針)在實際使用中,cap 必定是大於或等於 len 的。不然會致使 panicui
爲了更好的理解,咱們回顧上小節的代碼便於演示,以下:spa
func main() { nums := [3]int{} nums[0] = 1 dnums := nums[:] fmt.Printf("dnums: %v", dnums) }
在代碼中,可觀察到 dnums := nums[:]
,這段代碼肯定了 Slice 的 Pointer 指向數組,且 len 和 cap 都爲數組的基礎屬性。與圖示表達一致指針
func main() { nums := [3]int{} nums[0] = 1 dnums := nums[0:2] fmt.Printf("dnums: %v, len: %d, cap: %d", dnums, len(dnums), cap(dnums)) }
dnums: [1 0], len: 2, cap: 3
顯然,在這裏指定了 Slice[0:2]
,所以 len 爲所引用元素的個數,cap 爲所引用的數組元素總個數。與期待一致 😄
Slice 的建立有兩種方式,以下:
var []T
或 []T{}
func make([] T,len,cap)[] T
能夠留意 make 函數,咱們都知道 Slice 須要指向一個 Array。那 make 是怎麼作的呢?
它會在調用 make 的時候,分配一個數組並返回引用該數組的 Slice
func makeslice(et *_type, len, cap int) slice { maxElements := maxSliceCap(et.size) if len < 0 || uintptr(len) > maxElements { panic(errorString("makeslice: len out of range")) } if cap < len || uintptr(cap) > maxElements { panic(errorString("makeslice: cap out of range")) } p := mallocgc(et.size*uintptr(cap), et, true) return slice{p, len, cap} }
當使用 Slice 時,若存儲的元素不斷增加(例如經過 append)。當條件知足擴容的策略時,將會觸發自動擴容
那麼分別是什麼規則呢?讓咱們一塊兒看看源碼是怎麼說的 😄
func growslice(et *_type, old slice, cap int) slice { ... if et.size == 0 { if cap < old.cap { panic(errorString("growslice: cap out of range")) } return slice{unsafe.Pointer(&zerobase), old.len, cap} } ... }
當 Slice size 爲 0 時,若將要擴容的容量比本來的容量小,則拋出異常(也就是不支持縮容操做)。不然,將從新生成一個新的 Slice 返回,其 Pointer 指向一個 0 byte 地址(不會保留老的 Array 指向)
func growslice(et *_type, old slice, cap int) 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 } ... } } ... }
注:也就是小於 1024 個時,增加 2 倍。大於 1024 個時,增加 1.25 倍
func growslice(et *_type, old slice, cap int) slice { ... var overflow bool var lenmem, newlenmem, capmem uintptr const ptrSize = unsafe.Sizeof((*byte)(nil)) switch et.size { case 1: lenmem = uintptr(old.len) newlenmem = uintptr(cap) capmem = roundupsize(uintptr(newcap)) overflow = uintptr(newcap) > _MaxMem newcap = int(capmem) ... } if cap < old.cap || overflow || capmem > _MaxMem { panic(errorString("growslice: cap out of range")) } 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)) } } } ... }
一、獲取老 Slice 長度和計算假定擴容後的新 Slice 元素長度、容量大小以及指針地址(用於後續操做內存的一系列操做)
二、肯定新 Slice 容量大於老 Sice,而且新容量內存小於指定的最大內存、沒有溢出。不然拋出異常
三、若元素類型爲 kindNoPointers
,也就是非指針類型。則在老 Slice 後繼續擴容
capmem
,在老 Slice cap 後繼續申請內存空間,其後用於擴容add(p, newlenmem)
(ptr)注:那麼問題來了,爲何要從新初始化這塊內存呢?這是由於 ptr 是未初始化的內存(例如:可重用的內存,通常用於新的內存分配),其可能包含 「垃圾」。所以在這裏應當進行 「清理」。便於後面實際使用(擴容)
四、不知足 3 的狀況下,從新申請並初始化一塊內存給新 Slice 用於存儲 Array
五、檢測當前是否正在執行 GC,也就是當前是否啓用 Write Barrier(寫屏障),若啓用則經過 typedmemmove
方法,利用指針運算循環拷貝。不然經過 memmove
方法採起總體拷貝的方式將 lenmem 個字節從 old.array 拷貝到 ptr,以此達到更高的效率
注:通常會在 GC 標記階段啓用 Write Barrier,而且 Write Barrier 只針對指針啓用。那麼在第 5 點中,你就不難理解爲何會有兩種大相徑庭的處理方式了
這裏須要注意的是,擴容時的內存管理的選擇項,以下:
kindNoPointers
,將在老 Slice cap 的地址後繼續申請空間用於擴容func main() { nums := [3]int{} nums[0] = 1 fmt.Printf("nums: %v , len: %d, cap: %d\n", nums, len(nums), cap(nums)) dnums := nums[0:2] dnums[0] = 5 fmt.Printf("nums: %v ,len: %d, cap: %d\n", nums, len(nums), cap(nums)) fmt.Printf("dnums: %v, len: %d, cap: %d\n", dnums, len(dnums), cap(dnums)) }
輸出結果:
nums: [1 0 0] , len: 3, cap: 3 nums: [5 0 0] ,len: 3, cap: 3 dnums: [5 0], len: 2, cap: 3
在未擴容前,Slice array 指向所引用的 Array。所以在 Slice 上的變動。會直接修改到原始 Array 上(二者所引用的是同一個)
隨着 Slice 不斷 append,內在的元素愈來愈多,終於觸發了擴容。以下代碼:
func main() { nums := [3]int{} nums[0] = 1 fmt.Printf("nums: %v , len: %d, cap: %d\n", nums, len(nums), cap(nums)) dnums := nums[0:2] dnums = append(dnums, []int{2, 3}...) dnums[1] = 1 fmt.Printf("nums: %v ,len: %d, cap: %d\n", nums, len(nums), cap(nums)) fmt.Printf("dnums: %v, len: %d, cap: %d\n", dnums, len(dnums), cap(dnums)) }
輸出結果:
nums: [1 0 0] , len: 3, cap: 3 nums: [1 0 0] ,len: 3, cap: 3 dnums: [1 1 2 3], len: 4, cap: 6
往 Slice append 元素時,若知足擴容策略,也就是假設插入後,本來數組的容量就超過最大值了
這時候內部就會從新申請一塊內存空間,將本來的元素拷貝一份到新的內存空間上。此時其與本來的數組就沒有任何關聯關係了,再進行修改值也不會變更到原始數組。這是須要注意的
func copy(dst,src [] T)int
copy 函數將數據從源 Slice複製到目標 Slice。它返回複製的元素數。
func main() { dst := []int{1, 2, 3} src := []int{4, 5, 6, 7, 8} n := copy(dst, src) fmt.Printf("dst: %v, n: %d", dst, n) }
copy 函數支持在不一樣長度的 Slice 之間進行復制,若出現長度不一致,在複製時會按照最少的 Slice 元素個數進行復制
那麼在源碼中是如何完成複製這一個行爲的呢?咱們來一塊兒看看源碼的實現,以下:
func slicecopy(to, fm slice, width uintptr) int { if fm.len == 0 || to.len == 0 { return 0 } n := fm.len if to.len < n { n = to.len } if width == 0 { return n } ... size := uintptr(n) * width if size == 1 { *(*byte)(to.array) = *(*byte)(fm.array) // known to be a byte pointer } else { memmove(to.array, fm.array, size) } return n }
fm.array
複製 size
個字節到 to.array
的地址處(會覆蓋原有的值)在 Slice 中流傳着兩個傳說,分別是 Empty 和 Nil Slice,接下來讓咱們看看它們的小區別 🤓
func main() { nums := []int{} renums := make([]int, 0) fmt.Printf("nums: %v, len: %d, cap: %d\n", nums, len(nums), cap(nums)) fmt.Printf("renums: %v, len: %d, cap: %d\n", renums, len(renums), cap(renums)) }
輸出結果:
nums: [], len: 0, cap: 0 renums: [], len: 0, cap: 0
func main() { var nums []int }
輸出結果:
nums: [], len: 0, cap: 0
乍一看,Empty Slice 和 Nil Slice 好像如出一轍?不論是 len,仍是 cap 都爲 0。好像沒區別?咱們再看看以下代碼:
func main() { var nums []int renums := make([]int, 0) if nums == nil { fmt.Println("nums is nil.") } if renums == nil { fmt.Println("renums is nil.") } }
你以爲輸出結果是什麼呢?你可能已經想到了,最終的輸出結果:
nums is nil.
從圖示中能夠看出來,二者有本質上的區別。其底層數組的指向指針是不同的,Nil Slice 指向的是 nil,Empty Slice 指向的是實際存在的空數組地址
你能夠認爲,Nil Slice 代指不存在的 Slice,Empty Slice 代指空集合。二者所表明的意義是徹底不一樣的
經過本文,可得知 Go Slice 至關靈活。不須要你手動擴容,也不須要你關注加多少減多少。對 Array 是動態引用,是 Go 類型的一個極大的補充,也所以在應用中使用的更多、更便捷
雖然有個別要注意的 「坑」,但實際上是合理的。你以爲呢?😄