一文详解Go语言之Slice
一、什么是slice
slice翻译成中文是切片的意思,而在go编程中slice是一个数据类型,其代表一个列表,类似于java中的List。我们可以为每一种go中的基础类型或自定义类型创建对应的切片。在这里我们可以将slice理解成一个列表,而在日常开发中不管是使用什么语言,都需要经常用到列表这种数据结构,比如java中的List,我们在日常使用Java的开发中十分常见。而与java不同的是go将列表也就是slice作为一种基本类型,而不是List这样的封装类。
二、slice的结构
slice结构如下,其内部存放了指向底层数组的指针、len(长度)、cap(容量)
三、slice 实现原理
Slice又称动态数组,依托数组实现,可以方便的进行扩容、传递等,实际使用中比数组更灵活。Slice 依托数组实现,底层数组对用户屏蔽,在底层数组容量不足时可以实现自动重分配并生成新的 Slice 。
源码包中 src/runtime/slice.go:slice 定义了 Slice 的数据结构:
type slice struct {
array unsafe.Pointer
len int
cap int
}
从数据结构看Slice很清晰, array指针指向底层数组,len表示切片长度,cap表示底层数组容量。
3.1 使用make创建Slice
使用 make 来创建 Slice 时,可以同时指定长度和容量,创建时底层会分配一个数组,数组的长度即容量。例如,语句 slice := make([]int, 5, 10) 所创建的 Slice ,结构如下图所示:
该 Slice 长度为 5 ,即可以使用下标 slice[0] ~ slice[4] 来操作里面的元素, capacity 为 10 ,表示后续向 slice 添加新的元素时可以不必重新分配内存,直接使用预留内存即可。
3.2 使用数组创建Slice
使用数组来创建 Slice 时, Slice 将与原数组共用一部分内存。例如,语句 slice := array[5:7] 所创建的 Slice ,结构如下图所示:
切片从数组 array[5] 开始,到数组 array[7] 结束(不含 array[7] ),即切片长度为 2 ,数组后面的内容都作为切片的预留内存, 即capacity 为 5 。
数组和切片操作可能作用于同一块内存,这也是使用过程中需要注意的地方。
四、Slice 扩容
4.1、slice 扩容影响原数组
append 函数会改变 slice 所引用的数组的内容,从而影响到引用同一数组的其它 slice。但当 slice 中没有剩余空间(即(cap-len) == 0)时,此时将动态分配新的数组空间。返回的slice 数组指针将指向这个空间,而原数组的内容将保持不变;其它引用此数组的 slice 则不受影响。
func testSlice() {
var ar = [...]byte{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'}
// 打印结果:abcdefgh
fmt.Printf("%s \n", ar)
slice1 := ar[2:5]
// 注:append 函数会改变 slice 所引用的数组的内容,从而影响到引用同一数组的其它 slice。
// 但当 slice 中没有剩余空间(即(cap-len) == 0)时,此时将动态分配新的数组空间。返回的
// slice 数组指针将指向这个空间,而原数组的内容将保持不变;其它引用此数组的 slice 则不受影响。
slice1 = append(slice1, 'A')
// 打印结果:cdeA
fmt.Printf("%s \n", slice1)
// 打印结果:abcdeAgh
fmt.Printf("%s \n", ar)
}
4.2、slice 扩容注意事项
使用 append 向 Slice 追加元素时,如果 Slice 空间不足,将会触发 Slice 扩容,扩容实际上是重新分配一块更大的内存,将原Slice数据拷贝进新 Slice ,然后返回新 Slice ,扩容后再将数据追加进去。例如,当向一个 capacity 为 5 ,且 length 也为 5 的 Slice 再次追加 1 个元素时,就会发生扩容,如下图所示:
扩容操作只关心容量,会把原 Slice 数据拷贝到新 Slice ,追加数据由 append 在扩容结束后完成。上图可见,扩容后新的 Slice 长度仍然是 5 ,但容量由 5 提升到了 10 ,原 Slice 的数据也都拷贝到了新 Slice 指向的数组中。
4.3、扩容容量的选择遵循以下规则:
如果原Slice 容量小于 1024 ,则新 Slice 容量将扩大为原来的 2 倍;
如果原Slice 容量大于等于 1024 ,则新 Slice 容量将扩大为原来的 1.25 倍;
4.4、使用append()向Slice添加一个元素的实现步骤如下:
1、假如Slice 容量够用,则将新元素追加进去, Slice.len++ ,返回原 Slice
2、原Slice 容量不够,则将 Slice 先扩容,扩容后得到新 Slice
3、将新元素追加进新Slice , Slice.len++ ,返回新的 Slice 。
五、常见的slice的坑
slice到底是值传递还是引用传递?
对于这个问题我相信很多人都对此有争议,我们先来看一段代码:
在上面这段代码中,我们定义了一个长度len为5的切片s,并将s赋值给了t,之后我们将t[0]的值修改成了99,最终我们发现,s[0]的值也发生了改变,以当前的现象来看slice是引用传递,我们先不急,再来看一段代码。
在这段代码中我们依然定义了一个长度为5,容量为10的切片s,并将s赋值给了t,然后向t中添加了一个元素6。分别打印s跟t,我们发现这次打印出来的内容并不一致,从这个现象看来又好像是值传递。这时候可能有些人会有些疑惑,为什么同时表现出了值传递与引用传递的现象。我们再来看一段代码。
这次我们在代码2的基础上分别打印了 s 与 t 的len、cap与底层数组的地址,我们发现 s 的len为5,t 的len为6,除此之外cap与底层数组的地址都是一致的。因此我们可以得出结论,slice在go中的应该是值传递,只不过当将 s 赋值给 t 时底层数组指针指向的是同一个底层数组,而len与cap都是拷贝的副本,所以 t 在append之后len发生变化而 s 中的len并不会因此发生改变,其传递过程如下:
文章参考链接:
https://blog.csdn.net/xiaohei_buhei/article/details/122681538
https://blog.csdn.net/qq_40880022/article/details/123997549