一文详解Go语言之Slice

程序员面试吧

共 3252字,需浏览 7分钟

 · 2022-05-23

一、什么是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、扩容容量的选择遵循以下规则:

  1. 如果原Slice 容量小于 1024 ,则新 Slice 容量将扩大为原来的 2 倍;

  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并不会因此发生改变,其传递过程如下:

切片slice的坑非常多,在这也只是给大家展示了一种,经常有很多学员给我说:明明自己已经很小心了,但千防万防还是掉进slice的坑里了。

为了帮助广大学员对Go语言中切片(slice)有一个系统而全面的掌握,避免掉坑,我们特别邀请张朝阳老师为大家开启一场《slice避坑指南》的直播之旅。


张朝阳老师是华中科技大学的硕士,资深Go语言专家,多次举行Go专题公开课。可以说听完张朝阳老师的这节课,你会对Go语言中切片(slice)有一个非常全面的认识,完美避开slice中遇到的95%的坑。

本次课程向所有学员免费公开,课程难易程度上大家也完全不用担心,张朝阳老师有深厚的授课功底,非常擅于将抽象的知识具体化,所以只要你有一定Go语言的基础,就听得懂、学的会。

本次直播时间:5月19日晚8点

扫码进入直播间
👇👇👇


文章参考链接:

https://blog.csdn.net/xiaohei_buhei/article/details/122681538

https://blog.csdn.net/qq_40880022/article/details/123997549


浏览 76
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报