先提几个问题。声明一个slice,赋值给nil,比如varslice[]int=nil。此时len(slice)的结果是什么?func(arr[]int)和func(arr[10]int)两个函数都在内部修改arr,是否影响外部值(数据作为参数)?创建一个slice:=make([]int,5,10),那么slice[8]和slice[:8]的结果是什么?以下两段代码的输出是什么slice:=[]int{1,2,3,4,5}slice2:=append(slice[:3],6,7)fmt.Println(slice)fmt.Println(slice2)slice:=[]int{1,2,3,4,5}slice2:=append(slice[:3],6,7,8)//添加一个额外的数字8,这是唯一的区别是fmt.Println(slice)fmt.Println(slice2)如果以上问题都能轻松回答,可以直接关闭文章。为方便起见,以下描述均使用int作为元素类型来描述数组。首先,让我们谈谈数组。确实,在Go语言中,因为切片的存在,数组的出现率并不高。但是要想理解好slice,就必须先理解array。数组声明Go语言中的数组与其他语言相同。没有什么特别的。它是以元素类型(如int)为单位的连续内存空间。创建数组时,它被初始化为元素类型的零值。声明示例:vararr[10]int//长度为10的数组,默认所有元素为0arr:=[...]int{1,2,3}//长度由初始化的个数指定元素,这里长度为3arr:=[...]int{11:3}//长度为11的数组,arr[11]初始化为3,其他为0arr:=[5]int{1,2}//长度为5的数组,前两位初始化为1,2arr:=[5]int{1,2}//长度为5的数组,前两位初始化为1,2arr:=[...]int{1:23,2,3:22}//长度为4的数组,初始化为[023222][]设置数组的长度,写成...表示长度由以下初始化值决定。数组初始化的完整写法是{1:23,2:8,3:12},但是索引可以省略,写成{23,8,12},索引会自动从0开始累加。最大索引值决定了数组的长度。比如{5:10,11,12,6:100}是非法的,因为会被转换为{5:10,6:11,7:12,6:100},编译会出现Errorduplicateindexinarrayliteral:6.长度为0的数组特殊的是[0]int,长度为0的数组,这种不占用任何内存空间的数据类型其实是没有意义的,所以Go语言中这种数据经过特殊处理,还包括struct{}、[10]struct{}等,看个例子:var(a[0]intbstruct{}c[0]struct{valueint64}d[10]struct{}e=new([10]struct{})//new返回指针fbyte)fmt.Printf("%p,%p,%p,%p,%p,%p",&a,&b,&c,&d,e,&f)//0x1127a88,0x1127a88,0x1127a88,0x1127a88,0x1127a88,0xc42000e280前五个变量的内存地址相同,第六个变量f有真正的可用内存。也就是说,Go并没有真正为[0]int、struct{}等数据分配地址空间,而是统一使用同一个地址空间。map中经常用到这种数据结构,比如map[string]struct{}。声明这样的映射类型以标记键是否存在。在key值比较多的情况下,比map[string]bool等结构节省大量内存,也能减轻GC压力。数组用作函数参数。文章第一个问题提到,在func(arr[3]int)中内部修改arr是否会影响外部的实际值。答案是不。因为当使用数组作为参数时,会复制一份作为参数,函数内部操作的数组和外部数组在内存中根本不在同一个地方。是值传递,不是引用传递,这一点在某些语言中可能有所不同。请参见以下代码:array:=[3]int{1,2,3}func(innerArray[3]int){innerArray[0]=8fmt.Printf("%p:%v\n",&innerArray,innerArray)}(array)fmt.Printf("%p:%v\n",&array,array)//0xc42000a2e0:[823]//0xc42000a2c0:[123]函数内外,内存地址阵法不同,自然不会影响。如果想直接修改函数,可以使用指针,即func(arr*[3]int)。Slices通常用来表示一个变长的序列,也是基于数组实现的看下图:图中的Q2和summer是切片,其实是对数组months的引用,只是记录了数组中那些元素的引用。再看看Go中slice的定义。typeslicestruct{arrayunsafe.Pointer//引用数组中起始元素的地址lenint//lengthcapint//最大长度}我们在读写slice的时候,实际上是对它指向的数组进行操作。看到上面的slice数据结构,自然知道以下两点:值为nil的slice变量的len和cap都是0,虽然没有指向具体的数组(slice.array为空),但是它的slice.len和slice.cap默认为0.func(arr[]int)这个函数对参数arr的修改会影响外面的值,因为函数里面操作的内存和外面是一样的。这是切片和数组之间的主要区别之一。sliceoutofboundsslice是Scalable和variablelength,很多人误认为slice不会越界,下面来说明几种越界的情况。以上图右边的summer为例,summer[4]="hello"肯定会出现indexoutofrange的panic信息,虽然cap(summer)=7,但是summer[4]超出了len(summer)=3的范围。看下面的例子:arr:=[...]int{1,2,3,4,5,6,7,8,9,10}fmt.Println(arr[:3:5][:4])//[1234]fmt.Println(arr[:3:5][:8])//恐慌:运行时错误:sliceboundsoutofrangearr[:3:5]基于arr创建一个slice,len为3,cap为5;然后根据这个slice创建一个slice,len=4,len=8。前者运行正常,后者因为超出cap=5的范围而panic,虽然后者实际上所需的内存不超过arr数组的范围。切片操作记住两点:当直接访问数据时(slice[index]),索引值不能超过len(slice)创建切片的范围(slice[start:end]),由指定的范围start和end不能超过cap(slice)范围。所以文章开头的第三个问题,slice[8]会panic,但是slice[:8]会正常返回。很多人认为slice可以自动扩容,很可能是被append函数误导了。其实slice本身并不会自动扩容,而是在追加数据的时候,函数发现超过了caplimit会自动帮我们扩容。在执行append(slice,v1,v2)时,append函数会先检查执行结果的长度是否超过cap(slice)。如果超过了,先做一个更长的slice,然后把整个slice复制到新的slice,然后append。如果不是,直接添加len(slice)作为起点,len(slice)会随着append操作继续扩容,直到到达cap(slice)进行扩容。建议用户尽量避免让append自动帮你扩内存。一是因为扩容的时候会有内存拷贝,二是因为append不知道自己需要扩容多少。为了避免频繁扩容,会扩容到2*cap(slice)长度。有时我们不需要那么多内存。所以在使用创建切片的时候,最好不要直接appendwithoutmake,让它自己展开;而是先make([]int,0,capValue)准备一块内存,capValue需要自己估计,尽量保证够用就好。
