slice 实现原理
- slice又称动态数组,依托数组实现,可以方便的进行扩容、传递等
- slice依托数组实现,底层数组对用户屏蔽,在底层数组容量不足时可以实现自动重分配并生成新的Slice。
- go语言切片是对数组的抽象。也可以被看做为"动态数组",数组的长度不可改变,切片长度可变
- 在做函数调用时,slice 按引用传递,array 按值传递;
# slice 数据结构
切片的结构包括三个部分:
- 地址: 切片的地址一般指切片中的第一个元素所指向的内存地址, 用十六进制表示;
- 长度: 切片实际存在元素的个数;
- 容量: 从切片的起始元素开始到其底层数组中最后一个元素的个数;
type slice struct { array unsafe.Pointer len int cap int }
1
2
3
4
5
# slice 创建
# 1. 使用make创建slice
使用make来创建Slice时,可以同时指定长度和容量,创建时底层会分配一个数组,数组的长度即容量。
slice := make([]int, 5, 10)
所创建的Slice,结构如下图所示:该Slice长度为5,即可以使用下标slice[0] ~ slice[4]来操作里面的元素,capacity为10,表示后续向slice添加新的元素时可以不必重新分配内存,直接使用预留内存即可
# 2. 使用array创建slice
使用数组来创建Slice时,Slice将与原数组共用一部分内存。
切片从数组array[5]开始,到数组array[7]结束(不含array[7]),即切片长度为2,数组后面的内容都作为切片的预留内存,即capacity为5
# slice 扩容
使用append向Slice追加元素时,如果Slice空间不足,将会触发Slice扩容,扩容实际上是重新分配一块更大的内存,将原Slice数据拷贝进新Slice,然后返回新Slice,扩容后再将数据追加进去。
扩容容量的选择遵循以下规则:
- 如果原Slice容量小于1024,则新Slice容量将扩大为原来的2倍;
- 如果原Slice容量大于等于1024,则新Slice容量将扩大为原来的1.25倍;
使用append()向Slice添加一个元素的实现步骤如下:
- 假如Slice容量够用,则将新元素追加进去,Slice.len++,返回原Slice
- 原Slice容量不够,则将Slice先扩容,扩容后得到新Slice
- 将新元素追加进新Slice,Slice.len++,返回新的Slice。
# slice copy
使用copy()内置函数拷贝两个切片时,会将源切片的数据逐个拷贝到目的切片指向的数组中,拷贝数量取两个切片长度的最小值。长度为10的切片拷贝到长度为5的切片时,将会拷贝5个元素。
copy过程中不会发生扩容。
# slice 中的坑
# 1. 数组坑
code
func foo(a [2]int) { a[0] = 200 } func main() { a := [2]int{1, 2} foo(a) fmt.Println(a) // {1,2} }
1
2
3
4
5
6
7
8
9
10在 Go 语言中,数组是一种值类型,而且不同长度的数组属于不同的类型。例如
[2]int
和[20]int
属于不同的类型。当值类型作为参数传递时,参数是该值的一个拷贝,因此更改拷贝的值并不会影响原值。
为了避免数组的拷贝,提高性能,建议传递数组的指针作为参数,或者使用切片代替数组。
# 2. 扩容坑
code
func foo(a []int) { a = append(a, 1, 2, 3, 4, 5, 6, 7, 8) a[0] = 200 } func main() { a := []int{1, 2} foo(a) fmt.Println(a) // {1,2} }
1
2
3
4
5
6
7
8
9
10切片是引用类型, 函数传参是引用传参, 但是foo函数中对a 进行append, 会对a扩容, a扩容后产生了新的切片a, 然后对新的切片a修改元素是不会对main函数中的a产生影响
如果要对原来的切片进行更改
func foo(a *[]int) { *a = append(*a, 1, 2, 3, 4, 5, 6, 7, 8) (*a)[0] = 200 } func main() { a := []int{1, 2} foo(&a) fmt.Println(a) }
1
2
3
4
5
6
7
8
9
10
# 3. 内存坑
在已有切片的基础上进行切片,不会创建新的底层数组。因为原来的底层数组没有发生变化,内存会一直占用,直到没有变量引用该数组。因此很可能出现这么一种情况,原切片由大量的元素构成,但是我们在原切片的基础上切片,虽然只使用了很小一段,但底层数组在内存中仍然占据了大量空间,得不到释放。比较推荐的做法,使用
copy
替代re-slice
。func lastNumsBySlice(origin []int) []int { return origin[len(origin)-2:] } func lastNumsByCopy(origin []int) []int { result := make([]int, 2) copy(result, origin[len(origin)-2:]) return result }
1
2
3
4
5
6
7
8
9
# 总结
在做函数调用时,slice 按引用传递,array 按值传递;
每个切片都保存了当前切片的长度、底层数组可用容量;
使用len()计算切片长度时间复杂度为O(1),不需要遍历切片;
使用cap()计算切片容量时间复杂度为O(1),不需要遍历切片;
通过函数传递切片时,不会拷贝整个切片,因为切片本身只是个结构体而已;
使用append()向切片追加元素时有可能触发扩容,扩容后将会生成新的切片;
创建切片时可根据实际需要预分配容量,尽量避免追加过程中扩容操作;
切片拷贝时需要判断实际拷贝的元素个数;
谨慎使用多个切片操作同一个数组,以防读写冲突;