答案1:(溪尾)面试企业: 深信服、知乎、跟谁学
面试解析: GOLANG ROADMAP社区
数组长度是固定的,而切片是可变长的。可以把切片看作是对底层数组的封装,每个切片的底层数据结构中,一定会包含一个数组。数组可以被称为切片的底层数组,切片也可以被看作对数组某一连续片段的引用。因此,Go中切片属于引用类型,而数组属于值类型,通过内建函数len,可以取得数组和切片的长度。通过内建函数cap,可以得到数组和切片的容量。但是数组的长度和容量是相等的,并且都不可变,而且切片容量是有变化规律的。
(行飞子)数组和切片的关系:
切片一旦初始化, 切片始终与保存其元素的基础数组相关联。因此,切片会和与其拥有同一基础数组的其他切片共享存储 ; 相比之下,不同的数组总是代表不同的存储。
数组和切片的区别
切片的长度可能在执行期间发生变化 ,而数组的长度不能变化,可以把切片看成一个长度可变的数组。
数组作为函数参数是进行值传递的,函数内部改变传入的数组元素值不会影响函数外部数组的元素值; 切片作为函数的参数是进行的指针传递,函数内部改变切片的值会影响函数外部的切片元素值。
数组可以比较,切片不能比较(对底层数组的引用)。
1. Go切片和Go数组
Go切片,又称动态数组,它实际是基于数组类型做的一层封装。
Go数组
数组是内置(build-in)类型,是一组同类型数据的集合,它是值类型,通过从0开始的下标索引访问元素值。在初始化后长度是固定的,无法修改其长度。当作为方法的参数传入时将复制一份数组而不是引用同一指针。数组的长度也是其类型的一部分,通过内置函数len(array)获取其长度。
Go数组与像C/C++等语言中数组略有不同,如下
[10]int[20]int
Go切片
Go语言中数组的长度是固定的,且不同长度的数组是不同类型,这样的限制带来不少局限性。
而切片则不同,切片(slice)是一个拥有相同类型元素的可变长序列,可以方便地进行扩容和传递,实际使用时比数组更加灵活,这也正是切片存在的意义。
切片是引用类型,因此在当传递切片时将引用同一指针,修改值将会影响其他的对象。
2. 切片底层
现在就来看一下Go语言切片的底层是什么样子吧!
src/runtime/slice.go
可以看到,组成Go切片的三元组分别为指向底层数组的指针,切片长度和切片容量。
lenlen()capcap()
下图展示了一个Go切片的底层数据结构,这个切片的长度为3,容量为6。

3. 切片使用
1.切片定义方式
2.从数组中切取切片
数组和切片是紧密相连的。切片可以用来访问数组的部分或全部元素,而这个数组称为切片的底层数组。切片的指针指向数组第一个可以从切片中访问的元素,这个元素并不一定是数组的第一个元素。
一个底层数组可以对应多个切片,这些切片可以引用数组的任何位置,彼此之前的元素可以重叠。
s[i:j]
s[0:j]s[i:len(s)]
月份名称字符串数组与其对应的两个元素重叠的slice 图示

注意:切片与原数组或切片共享底层空间,修改切片会影响原数组或切片
1.迭代切片
切片可以用range迭代,但是要注意:如果只用一个值接收range,则得到的只是切片的下标,用两个值接收range,则得到的才是下标和对应的值。
2.切片拷贝
copy
例如长度为10的切片拷贝到长度为5的切片时,将拷贝5个元素。也就是说,拷贝过程中不会发生扩容。
copy函数有返回值,它返回实际上复制的元素个数,这个值就是两个slice长度的较小值。
4. 切片扩容-append函数
追加元素
append()
使用append()函数也可以在切片头部添加元素
注:从头部添加元素会引起内存的重分配,导致已有元素全部复制一次。因此从头部添加元素的开销要比从尾部添加元素大很多
通过append()函数链式操作从中间插入元素
使用链式操作在插入元素,在内层append函数中会创建一个临式切片,然后将a[i:]内容复制到新创建的临式切片中,再将临式切片追加至a[:i]中。
通过append()和copy()函数组合从中间插入元素
使用这种方式可以避免创建过程中间的临式切片,也可以做到从中间插入元素
使用此方式虽然稍显复杂,但是可以减少创建中间临时切片的开销。
删除元素
很遗憾,Go语言中并没有提供直接删除指定位置元素的方式。不过根据切片的性质,我们可以通过巧妙的拼接切片来达到删除指定数据的目的。
slice扩容
appendappend
使用append向slice追加元素时,如果slice空间不足,则会触发slice扩容,扩容实际上是分配一块更大的内存,将原slice的数据拷贝进新slice,然后返回新slice,扩容后再将数据追加进去。

扩容操作只关心容量,会把原slice的数据拷贝至新slice中,追加数据由append在扩容后完成。由上图可见,扩容后新slice的长度仍然是5,但容量由5提到了10,原slice的数据也都拷贝到了新的slice指向的数组中。
扩容容量的选择遵循以下基本规则
如果原slice的容量小于1024,则新slice的容量将扩大为原来的2倍;
如果原slice的容量大于1024,则新的slice的容量将扩大为原来的1.25倍;
5. Go切片,Python切片,都是切片,有什么不同?
Go有切片slice类型,Python有列表和元组,这两种语言都有切片操作。但是它们的切片操作是完全不同的。
最大的不同就是
Python的切片产生的是新的对象,对新对象的成员的操作不影响旧对象;
Go的切片产生的是旧对象一部分的引用,对其成员的操作会影响旧对象;
究其原因还是底层实现不同
Go的切片,底层是一个三元组。指针指向一块连续的内存,长度是已有成员数,容量是最大成员数。切片时,一般并不会申请新的内存,而是对原指针进行移动,然后和新的长度、容量组成一个切片类型值返回。也就是说,Go的切片操作通常会和生成该切片的切片或数组共享内存。
Python的切片,其实就是指针数组。对它进行切片,会创建新的数组。在Python的切片中,并没有容量的概念。
这其实也体现了脚本语言和编译语言的不同。虽然两个语言都有类似的切片操作;但是Python主要目标是方便;Go主要目标却是快速。
在使用中,Go切片和Python切片也有很多不同
[a:b:c]
6. 切片陷阱
1.无法做比较
bytes.Equal
slice唯一允许的比较操作是和nil进行比较,例如
2.空切片和nil切片
空切片和nil切片是不同的。
nil切片中,切片的指针指向的是空地址,其长度和容量都为零。nil切片和nil相等。
空切片,切片的指针指向了一个地址,但其长度和容量也为0,和nil不相等,通常用来表示一个空的集合。
3.使用range进行切片迭代
当使用range进行切片迭代时,range创建了每个元素的副本,而不是直接返回对该元素的引用。如果使用该值变量的地址作为每个元素的指针,就会造成错误。
从结果中可以看出,使用range进行迭代时,v的地址是始终不变的,它并不是切片中每个变量的实际地址。而是在使用range进行遍历时,将切片中每个元素都复制到了同一个变量v中。如果错误的将v的地址当作切边元素的地址,将会引发错误。
4.切片扩容引发的问题
正因为有扩容机制。所以我们无法保证原始的slice和用append后的结果slice指向同一个底层数组,也无法证明它们就指向不同的底层数组。同样,我们也无法假设旧slice上对元素的操作会或者不会影响新的slice元素。所以,通常我们将append的调用结果再次赋给传入append的slice。
内置append函数在向切片追加元素时,如果切片存储容量不足以存储新元素,则会把当前切片扩容并产生一个新的切片。
append函数每次追加元素都有可能触发切片扩容,即有可能返回一个新的切片,这正是append函数声明中返回值为切片的原因,使用时应该总是接收该返回值。
建议
使用append函数时,谨记append可能会产生新的切片,并谨慎的处理返回值。
5.append函数误用
使用append函数时,需要考虑append返回的切片是否跟原切片共享底层的数组。下面这段程序片段,来看看函数返回的结果。
题目首先创建了一个长度为0,容量为10的切片x,然后向切片x追加了1,2,3三个元素。其底层的数组结构如下图所示

创建切片y为切片x追加一个元素4后,底层数组结构如下图所示

需要注意的是切片x仍然没有变化,切片x中记录的长度仍为3。继续向x追加元素5后,底层数组结构如下图所示

至此,答案已经非常明确了。当向x继续追加元素5后,切片y的最后一个元素被覆盖掉了。
此时切片x仍然为[1 2 3],而切片y和z则为[1 2 3 5]。
建议
一般情况下,使用append函数追加新的元素时,都会用原切片变量接收返回值来获得更新
6.函数传参
Go语言中将切片作为函数参数传递会有什么神奇的现象,一起来看看下面这个示例。
首先,我们创建了两个切片,a切片长度和容量均为3,b切片长度为1,容量为10。将a切片和b切片作为函数参数传入test函数中。
在test函数中,对a切片和b切片做了如下两点改动
分别使用append函数在a切片和b切片中追加一个元素
分别对a切片和b切片的第一个元素做了修改
分别在主函数中和test函数中输出两个切片,会发现在主函数中和test函数中两个切片好像改了,又好像没改,下面我们就来分析一下。
理论分析
当我们将一个切片作为函数参数传递给函数的时候,采用的是值传递,因此我们传递给函数的参数其实是上面这个切片三元组的值拷贝。当我们对切片结构中的指针进行值拷贝的时候,得到的指针还是指向了同一个底层数组。因此我们通过指针对底层数组的值进行修改,从而修改了切片的值。
lencaplencap
appendlen
题目再分析
有了前面的理论基础,我们再来分析一下a,b切片的返回结果。
a切片作为参数传至test函数中,在test中向a切片追加一个元素后,此时触发扩容机制,返回的切片已经不再是原切片,而是一个新的切片。后续对a切片中的第一个元素进行修改也是对新切片进行修改,对老切片不会产生任何影响。
所以,最终在主函数中a切片仍然为[1 2 3],而在test函数中a切片变成了[3 2 3 4]。
b切片作为参数传至test函数中,在test中向b切片追加一个元素后,不会触发扩容机制,返回的仍然是原切片,所以在后续对b切片的修改都是在原切片中进行的修改。故在test函数中b切片为[3 2]。但是在主函数中确为[3],可以看出在test中对切片进行修改确实反应到主函数中了,但是由于其len和cap没有改变,len仍为1,所以最终就只输出切片中的第一个元素[3],但其底层数组的值其实已经改变了。
查看全部560道企业Go面试真题,请访问GOLANG ROADMAP社区:www.golanngroadmap.com