原文地址:深刻理解 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 类型的一个极大的补充,也所以在应用中使用的更多、更便捷
虽然有个别要注意的 “坑”,但实际上是合理的。你以为呢?😄