切片也是一种数据结构,它和数组很是类似,由于他是围绕动态数组的概念设计的,能够按需自动改变大小,使用这种结构,能够更方便地管理和使用数据集合。算法
内部实现数组
切片是基于数组实现的,它的底层是数组,它本身自己很是小,能够理解为对底层数组的抽象。由于机遇数组实现,因此它的底层的内存是连续非配的,效率很是高。它还有能够经过索引得到数据、能够迭代以及垃圾回收优化的好处。数据结构
切片对象很是小,是由于它是只有 3 个字段的数据结构:一个是指向底层数组的指针,一个是切片的长度,一个是切片的容量。这 3 个字段,就是Go语言操做底层数组的元数据,有了它们,咱们就能够任意地操做切片了。app
声明和初始化ide
切片建立的方式有好几种,咱们先看下最简洁的make方式。函数
slice:=make([]int,5)
使用内置的make
函数时,须要传入一个参数,指定切片的长度,例子中咱们使用的时 5 ,这时候切片的容量也是 5 。固然咱们也能够单独指定切片的容量。优化
slice:=make([]int,5,10)
这时,咱们建立的切片长度是 5 ,容量是 10 。须要注意的这个容量 10 其实对应的是切片底层数组的。spa
由于切片的底层是数组,因此建立切片时,若是不指定字面值的话,默认值就是数组的元素的零值。这里咱们因此指定了容量是 10 ,可是咱们只能访问 5 个元素。由于切片的长度是 5 ,剩下的 5 个元素,须要切片扩充后才能够访问。设计
容量必须>=长度,咱们是不能建立长度大于容量的切片的。指针
还有一种建立切片的方式,是使用字面量,就是指定初始化的值。
slice:=[]int{1,2,3,4,5}
有没有发现,跟建立数组很是像,只不过不用制定[]
中的值。这时候切片的长度和容量是相等的,而且会根据咱们指定的字面量推导出来。固然咱们也能够像数组同样,只初始化某个索引的值:
slice:=[]int{4:1}
这是指定了第 5 个元素为 1 ,其余元素都是默认值 0 。这时候切片的长度和容量也是同样的。这里再次强调一下切片和数组的微小差异。
//数组 array:=[5]int{4:1} //切片 slice:=[]int{4:1}
切片还有nil切片和空切片,它们的长度和容量都是 0 。可是它们指向底层数组的指针不同,nil切片意味着指向底层数组的指针为nil,而空切片对应的指针是个地址。
//nil切片 var nilSlice []int //空切片 slice:=[]int{}
nil切片表示不存在的切片,而空切片表示一个空集合,它们各有用处。
切片另一个用处比较多的建立是基于现有的数组或者切片建立。
slice := []int{1, 2, 3, 4, 5} slice1 := slice[:] slice2 := slice[0:] slice3 := slice[:5] fmt.Println(slice1) fmt.Println(slice2) fmt.Println(slice3)
基于现有的切片或者数组建立,使用[i:j]
这样的操做符便可。它表示以i
索引开始,到j
索引结束,截取原数组或者切片,建立而成的新切片。新切片的值包含原切片的i
索引,可是不包含j
索引。对比Java的话,发现和String的subString
方法很像。
i
若是省略,默认是 0 ;j
若是省略,默认是原数组或者切片的长度。因此例子中的三个新切片的值是同样的。这里注意的是i
和j
都不能超过原切片或者数组的索引。
slice := []int{1, 2, 3, 4, 5} newSlice := slice[1:3] newSlice[0] = 10 fmt.Println(slice) fmt.Println(newSlice)
这个例子证实了,新的切片和原切片共用的是一个底层数组。因此当修改的时候,底层数组的值就会被改变,因此原切片的值也改变了。固然对于基于数组的切片也同样的。
咱们基于原数组或者切片建立一个新的切片后,那么新的切片的大小和容量是多少呢?这里有个公式:
对于底层数组容量是k的切片slice[i:j]来讲 长度:j-i 容量:k-i
好比咱们上面的例子slice[1:3]
,长度就是3-1=2
,容量是5-1=4
。不过代码中咱们计算的时候不用这么麻烦,由于Go语言为咱们提供了内置的len
和cap
函数来计算切片的长度和容量。
slice := []int{1, 2, 3, 4, 5} newSlice := slice[1:3] fmt.Printf("newSlice长度:%d,容量:%d",len(newSlice),cap(newSlice))
以上是基于一个数组或者切片使用 2 个索引建立新切片的方法。此外还有一种 3 个索引的方法,第 3 个用来限定新切片的容量,其用法为slice[i:j:k]
。
slice := []int{1, 2, 3, 4, 5} newSlice := slice[1:2:3]
这样咱们就建立了一个长度为2-1=1
,容量为3-1=2
的新切片,不过第三个索引,不能超过原切片的最大索引值 5 。
使用切片
使用切片,和使用数组同样,经过索引就能够获取切片对应元素的值,一样也能够修改对应元素的值。
slice := []int{1, 2, 3, 4, 5} fmt.Println(slice[2]) //获取值 slice[2] = 10 //修改值 fmt.Println(slice[2]) //输出10
切片只能访问到其长度内的元素,访问超过长度外的元素,会致使运行时异常,与切片容量关联的元素只能用于切片增加。
咱们前面讲了,切片算是一个动态数组,因此它能够按需增加,咱们使用内置append
函数便可。append
函数能够为一个切片追加一个元素,至于如何增长、返回的是原切片仍是一个新切片、长度和容量如何改变这些细节,append
函数都会帮咱们自动处理。
slice := []int{1, 2, 3, 4, 5} newSlice := slice[1:3] newSlice=append(newSlice,10) fmt.Println(newSlice) fmt.Println(slice) //Output [2 3 10] [1 2 3 10 5]
例子中,经过append
函数为新建立的切片newSlice
,追加了一个元素 10 。咱们发现打印的输出,原切片slice
的第 4 个值也被改变了,变成了 10 。引发这种结果的缘由是由于newSlice
有可用的容量,不会建立新的切片来知足追加,因此直接在newSlice
后追加了一个元素 10 。由于newSlice
和slice
切片共用一个底层数组,因此切片slice
的对应的元素值也被改变了。
这里newSlice新追加的第 3 个元素,其实对应的是slice的第 4 个元素,因此这里的追加实际上是把底层数组的第4个元素修改成 10 ,而后把newSlice长度调整为 3 。
若是切片的底层数组没有足够的容量时,就会新建一个底层数组,把原来数组的值复制到新底层数组里,再追加新值,这时候就不会影响原来的底层数组了。
因此通常咱们在建立新切片的时候,最好要让新切片的长度和容量同样,这样咱们在追加操做的时候就会生成新的底层数组,和原有数组分离,就不会由于共用底层数组而引发奇怪问题,由于共用数组的时候修改内容,会影响多个切片。
append
函数会智能地增加底层数组的容量,目前的算法是:容量小于 1000 个时,老是成倍的增加;一旦容量超过 1000 个,增加因子设为 1.25 ,也就是说每次会增长 25% 的容量。
内置的append
也是一个可变参数的函数,因此咱们能够同时追加好几个值。
newSlice=append(newSlice,10,20,30)
此外,咱们还能够经过...
操做符,把一个切片追加到另外一个切片里。
slice := []int{1, 2, 3, 4, 5} newSlice := slice[1:2:3] newSlice=append(newSlice,slice...) fmt.Println(newSlice) fmt.Println(slice)
迭代切片
切片是一个集合,咱们可使用for range循环来迭代它,打印其中的每一个元素以及对应的索引。
slice := []int{1, 2, 3, 4, 5} for i,v:=range slice{ fmt.Printf("索引:%d,值:%d\n",i,v) }
若是咱们不想要索引,可使用_
来忽略它。这是Go语言的用法,不少不须要的函数等返回值,均可以忽略。
slice := []int{1, 2, 3, 4, 5} for _,v:=range slice{ fmt.Printf("值:%d\n",v) }
这里须要说明的是range
返回的是切片元素的复制,而不是元素的引用。
除了for range循环外,咱们也可使用传统的for循环,配合内置的len函数进行迭代。
slice := []int{1, 2, 3, 4, 5}
for i := 0; i < len(slice); i++ {
fmt.Printf("值:%d\n", slice[i])
}
在函数间传递切片
咱们知道切片是 3 个字段构成的结构类型,因此在函数间以值的方式传递的时候,占用的内存很是小,成本很低。在传递复制切片的时候,其底层数组不会被复制,也不会受影响,复制只是复制的切片自己,不涉及底层数组。
func main() { slice := []int{1, 2, 3, 4, 5} fmt.Printf("%p\n", &slice) modify(slice) fmt.Println(slice) } func modify(slice []int) { fmt.Printf("%p\n", &slice) slice[1] = 10 }
打印的输出以下:
0xc420082060 0xc420082080 [1 10 3 4 5]
仔细看,这两个切片的地址不同,因此能够确认切片在函数间传递是复制的。而咱们修改一个索引的值后,发现原切片的值也被修改了,说明它们共用一个底层数组。
在函数间传递切片很是高效,并且不须要传递指针和处理复杂的语法,只须要复制切片,而后根据本身的业务修改,最后传递回一个新的切片副本便可。这也是为何函数间使用切片传递参数,而不是数组的缘由。
关于多维切片就不介绍了,还有多维数组,一来它和普通的切片数组同样,只不过是多个一维组成的多维;二来我压根不推荐用多维切片和数组,可读性很差,结构不够清晰,容易出问题。