非懂不可的Slice(一)
2 --> 前言 切片是一种复合数据类型,与数组类似,存放相同数据类型的元素,但数组的大小是固定的,而切片的大小可变,可以按需自动改变大小。切片是基于底层数组实现的,是对数组的抽象。切片很小,只有三个字段的数据结构:指向底层数组的指针、能访问的元素个数(即切片长度)和允许增长到的元素个数(即切片容量)。 如上图所示,一个长度为3、容量为5的整型切片的底层结构。 声明与初始化 make()创建 使用内置函数创建空切片,形如: s:=make([]type,len,cap)//len长度,cap容量 也可以只指定len,那么切片的容量和长度是一样的。Go语言提供了内置函数len、cap分别返回切片的长度和容量。 //声明一个长度为3、容量为5的整型切片s1:=make([]int,3,5)fmt.Println(len(s1),cap(s1))// 输出:3 5//声明一个长度和容量都是5的字符串切片s2:=make([]string,5)fmt.Println(len(s2),cap(s2))// 输出:5 5 切片创建完成,如果不指定字面量的话,默认值就是数组的元素的零值。切片的容量就是切片底层数组的大小,我们只能访问切片长度范围内的元素,如第一节的图所示,长度为3的整型切片存入3个值后的结构,我们只能访问到第3个元素,剩余的2个元素需要切片扩充以后才可以访问。所以,很明显的:容量>=长度,我们不能创建长度大于容量的切片。 s1:=make([]int,5,3)// 报错:len larger than cap in make([]int) 使用字面量创建切片 使用字面量创建,就是指定了初始化的值 s:=[]int{1,2,3,4,5}//长度和容量都是5的整型切片 有没有发现,这种创建方式与创建数组类似,只不过不用指定[]的值,这时候切片的长度和容量是相等的,并且会根据指定的字面量推导出来。区别: //创建大小为10的数组s:=[10]int{1,2,3,4,5}//创建切片s:=[]int{1,2,3,4,5} 我们也可以只初始化某一个索引的值: s:=[]int{4:1}fmt.Println(len(s),cap(s))// 输出:5 5fmt.Println(s)// 输出:[0 0 0 0 1] 指定了第5个元素为1,其他元素初始化为0。 基于已有的数组或者切片创建切片 使用操作符[start,end],简写成[i,j],表示从索引i,到索引j结束,截取已有数组或者切片的任意部分,返回一个新的切片,新切片的值包含原切片的i索引的值,但是不包含j索引的值。i、j都是可选的,i如果省略,默认是0,j如果省略,默认是原切片或数组的长度。i、j都不能超过这个长度值。 s:=[]int{0,1,2,3,4,5,6,7,8,9}fmt.Println("s[:]",s[:])fmt.Println("s[2:]",s[2:])fmt.Println("s[:4]",s[:4])fmt.Println("s[2:4]",s[2:4]) 输出你可能会有个疑问:截取获得的新切片的长度和容量怎么计算呢?我们当然可以使用内置函数len、cap直接获得,如果明白了怎么计算的,我们处理问题就可以更得心应手。对底层数组大小为k的切片执行[i,j]操作之后获得的新切片的长度和容量是:长度:j-i容量:k-i就拿上一个例子的s[2:4]来说,原切片底层数组大小是10,所以新切片的长度是4-2=2,容量是10-2=8。可以使用内置函数验证下: s1:=s[2:4]fmt.Println(len(s1),cap(s1))// 输出:2 8 上面是使用2个索引的方法创建切片,还可以使用3个索引的方法,第3个用来限定新切片的容量,用法为slice[i:j:k]。 s2:=s[2:4:8]fmt.Println(s2)// 输出:[2 3] 长度和容量如何计算:长度j-i,容量k-i。所以切片s2的长度和容量分别是2、6。注意:k不能超过原切片(数组)的长度,否则报错panic: runtime error: slice bounds out of range。例如上面的例子中,第三个索引值不能超过10。我们来看个例子: s:=[]int{0,1,2,3,4,5}fmt.Println("before,s:",s)s1:=s[1:4]fmt.Println("before,s1:",s1)s1[1]=10fmt.Println("after,s1:",s1)fmt.Println("after,s:",s) 输出: before,s:[012345]before,s1:[123]after,s1:[1103]after,s:[0110345] 这个例子说明,原切片和新切片是基于同一个底层数组的,所以当修改的时候,底层数组的值就会被改变,原切片的值也随之改变了。对于基于数组的切片也一样的。 我们可以看到,执行完切片动作之后,获得一个新切片,与原切片共享同一段底层数组,但通过不同的切片会看到底层数组的不同部分。切片s能够看到底层数组全部6个元素,而切片s1只能看到索引1及之后的全部元素,对于s1来说,索引1之前的部分是不存在。 使用切片 切片的使用方法与数组的使用方法类似,直接通过索引就可以获取、修改元素的值。 s:=[]int{1,2,3,4,5}fmt.Println(s[1])// 获取值 输出:2s[1]=10//修改值fmt.Println(s)//输出:[1 10 3 4 5] 只能访问切片长度范围内的元素,否则报错 s:=[]int{1,2,3,4,5}s1:=s[2:3]fmt.Println(s1[1]) 上面这个例子中,s1的容量为3,长度为1,所以只能访问s1第一个元素s1[0],访问s1[1]就会报错:panic: runtime error: index out of range 与切片的容量相关联的元素只能用于增长切片,在使用这部分元素前,必须将其合并到切片的长度里。 相较于数组,使用切片的好处在于,可以按需增长,类似于动态数组。Go提供了内置append函数,能够帮我们处理切片增长的一些列细节,我们只管使用就可以了。函数原型: funcappend(slice[]Type,elems...Type)[]Type 使用append函数,需要一个被操作的切片和一个(多个)追加值,返回一个相同数据类型的新切片。 s:=[]int{1,2,3,4,5}newS:=s[2:4]newS=append(newS,50)fmt.Println(s,newS)fmt.Println(&s[2]==&newS[0]) 输出: [123450][3450]true 上面的例子中,截取获得一个长度为2,容量为3(可用容量为1)的新切片newS,通过append函数向切片newS追加一个元素50。追加元素50之前: 追加元素10之前 追加元素50之后: 追加元素10之后 通过输出结果可以得出,新切片newS与原切片s是共享底层数组的,当切片可用容量能够存下追加元素时,不会创建新的切片。 当切片可用容量存不下需要追加的元素时会发生呢? s:=[]int{1,2,3,4,5,6,7,8}s1:=s[2:4]fmt.Printf("before->s=%v\n",s)fmt.Printf("before->s1=%v\n",s1)fmt.Printf("before->len=%d,cap=%d\n",len(s1),cap(s1))fmt.Println("&s[2]==&s1[0]is",&s[2]==&s1[0])s1=append(s1,60,70,80,90,100,110)fmt.Printf("after->s=%v\n",s)fmt.Printf("after->s1=%v\n",s1)fmt.Printf("after->len=%d,cap=%d\n",len(s1),cap(s1))fmt.Println("&s[2]==&s1[0]is",&s[2]==&s1[0]) 输出: before->s=[12345678]before->s1=[34]before->len=2,cap=6&s[2]==&s1[0]istrueafter->s=[12345678]after->s1=[3460708090100110]after->len=8,cap=12&s[2]==&s1[0]isfalse 追加元素60、70、80、90、100和110之前: 追加之后: 从结果可以看出,切片的底层数组没有足够的可用容量, append函数会创建一个新的底层数组,将原数组的值复制到新数组里,再追加新的值,就不会影响原来的底层数组。 一般我们在创建新切片的时候,最好要让新切片的长度和容量一样,这样我们在追加操作的时候就会生成新的底层数组,和原有数组分离,就不会因为共用底层数组而引起奇怪问题,因为共用数组的时候修改内容,会影响多个切片。 append函数会智能地增加底层数组的容量,目前的算法是:当数组容量<=1024时,会成倍地增加;当超过1024,增长因子变为1.25,也就是说每次会增加25%的容量。Go提供了...操作符,允许将一个切片追加到另一个切片上: s:=[]int{1,2,3,4,5}s1:=[]int{6,7,8}s=append(s,s1...)fmt.Println(s,s1) 输出: [12345678][678] 迭代切片 使用for循环迭代切片,配合len函数使用: s:=[]int{1,2,3,4,5}fori:=0;i<len(s);i++{fmt.Printf("Index:%d,Value:%d\n",i,s[i])} 使用for range迭代切片: s:=[]int{1,2,3,4,5}fori,v:=ranges{fmt.Printf("Index:%d,Value:%d\n",i,v)}//使用‘_’可以忽略返回值s:=[]int{1,2,3,4,5}for_,v:=ranges{fmt.Printf("Value:%d\n",v)} 需要注意的是,range返回的是切片元素的复制,而不是元素的引用。如果使用该值变量的地址作为指向每个元素的指针,就会造成错误。 s:=[]int{1,2,3,4,5}fori,v:=ranges{fmt.Printf("v:%d,v_addr:%p,elem_addr:%p\n",v,&v,&s[i])} 输出: v:1,v_addr:0xc000018058,elem_addr:0xc000016120v:2,v_addr:0xc000018058,elem_addr:0xc000016128v:3,v_addr:0xc000018058,elem_addr:0xc000016130v:4,v_addr:0xc000018058,elem_addr:0xc000016138v:5,v_addr:0xc000018058,elem_addr:0xc000016140 可以看到,v的地址总是相同的,因为迭代返回的变量在迭代过程中根据切片依次赋值的新变量。好了,今天先讲到这里,下一节,我们再来讨论关于Slice更多的用法! 如果我的文章对你有所帮助,点赞、转发都是一种支持!