切片也是一种数据结构,它和数组非常相似,是围绕 动态数组 的概念设计的,可以按需自动改变大小,使用这种结构,可以更方便的管理和使用数据集合。分配在堆上。

本质是结构体

概念

  • 其本身并不是数组,它指向底层数组
  • 作为变长数组的替代方案,可以关联底层数组的局部或者全部
  • 为引用类型
  • 可以直接创建或从底层数组生成
  • 如果多个切片指向相同的底层数组,其中一个值改变会影响全部
  • 一般使用make创建,meke([]T,len,cap) cap省略时和len相同
  • len()获取元素个数,cap()获取容量

    内部实现

    切片是基于数组实现的,它的底层是数组,它自己本身非常小,可以理解为对底层数组的抽象。因为基于数组实现,所以它的底层的内存是连续分配的,效率非常高,还可以通过索引获得数据,可以迭代以及垃圾回收优化的好处。

切片对象非常小,是因为它是只有3个字段的数据结构:

  1. 指向底层数组的指针
  2. 切片的长度
  3. 切片的容量

这3个字段,就是Go语言操作底层数组的元数据,有了它们,我们就可以任意的操作切片了。

切片内部结构,实质是结构体

基本类型-切片-slice-结构体 - 图1

切片共享存储结构

基本类型-切片-slice-结构体 - 图2

声明和初始化

make方式

  1. slice := make([]int,5)
  2. slice := make([]int,5,10)

使用内置的make函数时,需要传入一个参数,指定切片的长度,例子中用的是5,这时候切片的容量也是5。可以第二个参数单独指定切片的容量,这个容量10其实对应的是切片底层数组的。

因为切片的底层是数组,所以创建切片时,如果不指定字面值的话,默认值就是数组的元素的零值。这里我们所以指定了容量是10,但是我们职能访问5个元素,因为切片的长度是5,剩下的5个元素,需要切片扩充后才可以访问,可以通过append扩充。

容量必须>=长度,我们是不能创建长度大于容量的切片的。

使用字面量

就是指定初始化的值。

  1. slice := []int{1,2,3,4,5}
  2. slice := []int{4:1}

此时切片的长度和容量是相等的,并且会根据我们指定的字面量推导出来。

基于现有的数组或者切片创建

  1. slice := []int{1, 2, 3, 4, 5}
  2. slice1 := slice[:]
  3. slice2 := slice[0:]
  4. slice3 := slice[:5]
  5. fmt.Println(slice1)
  6. fmt.Println(slice2)
  7. fmt.Println(slice3)

共用底层数组

  1. slice := []int{1, 2, 3, 4, 5}
  2. newSlice := slice[1:3]
  3. newSlice[0] = 10
  4. fmt.Println(slice)
  5. fmt.Println(newSlice)

这个例子证明了,新的切片和原切片共用的是一个底层数组,所以当修改的时候,底层数组的值就会被改变,所以原切片的值也改变了。当然对于基于数组的切片也一样的。

使用切片

使用切片,和使用数组一样,通过索引就可以获取切片对应元素的值,同样也可以修改对应元素的值。

  1. slice := []int{1, 2, 3, 4, 5}
  2. fmt.Println(slice[2]) //获取值
  3. slice[2] = 10 //修改值
  4. fmt.Println(slice[2]) //输出10

切片只能访问到其长度内的元素,访问超过长度外的元素,会导致运行时异常,与切片容量关联的元素只能用于切片增长。

append

切片算是一个动态数组,所以它可以按需增长,我们使用内置append函数即可。append函数可以为一个切片追加一个元素,至于如何增加、返回的是原切片还是一个新切片、长度和容量如何改变这些细节,append函数都会帮我们自动处理。
内置的append也是一个可变参数的函数,所以我们可以同时追加好几个值。

  1. days := [7]int{1, 2, 3, 4, 5, 6, 7}
  2. p1 := days[:]
  3. p2 := days[:]
  4. fmt.Println(days)
  5. fmt.Println(p1)
  6. fmt.Println(p2)
  7. p1 = append(p1, 8)
  8. p1[0] = 0
  9. fmt.Println(days)
  10. fmt.Println(p1)
  11. fmt.Println(p2)
  12. // out
  13. [1 2 3 4 5 6 7]
  14. [1 2 3 4 5 6 7]
  15. [1 2 3 4 5 6 7]
  16. [1 2 3 4 5 6 7]
  17. [0 2 3 4 5 6 7 8]
  18. [1 2 3 4 5 6 7]

一般我们在创建新切片的时候,最好要让新切片的长度和容量一样,这样我们在追加操作的时候就会生成新的底层数组,和原有数组分离,就不会因为共用底层数组而引起奇怪问题,因为共用数组的时候修改内容,会影响多个切片。

append函数会智能的增长底层数组的容量,目前的算法是:容量小于1000个时,总是成倍的增长,一旦容量超过1000个,增长因子设为1.25,也就是说每次会增加25%的容量。

copy 拷贝切片

这里的 copy 不是引用传递,切片起始指针会改变。

  1. s1 := []int{1, 2,3, 4, 5}
  2. s2 := make([]int, 5)
  3. copy(s2, s1)
  4. fmt.Printf("%p\n", s1)
  5. fmt.Printf("%p\n", s2)
  6. // out
  7. 0xc000118030
  8. 0xc000118060

迭代切片

for range循环

切片是一个集合,我们可以使用 for range 循环来迭代它,打印其中的每个元素以及对应的索引。

  1. slice := []int{1, 2, 3, 4, 5}
  2. for i,v:=range slice{
  3. fmt.Printf("index:%d, value:%d\n",i,v)
  4. }

传统的for循环

配合内置的len函数进行迭代。

  1. slice := []int{1, 2, 3, 4, 5}
  2. for i := 0; i < len(slice); i++ {
  3. fmt.Printf("值:%d\n", slice[i])
  4. }

在函数间传递切片

slice 是引用传递。
我们知道切片是3个字段构成的结构类型,所以在函数间以值的方式传递的时候,占用的内存非常小,成本很低。在传递复制切片的时候,其底层数组不会被复制,也不会受影响,复制只是复制的切片本身,不涉及底层数组。

  1. func main() {
  2. slice := []int{1, 2, 3, 4, 5}
  3. fmt.Printf("%p\n", &slice)
  4. modify(slice)
  5. fmt.Println(slice)
  6. }
  7. func modify(slice []int) {
  8. fmt.Printf("%p\n", &slice)
  9. slice[1] = 10
  10. }

打印的输出如下:

  1. 0xc420082060
  2. 0xc420082080
  3. [1 10 3 4 5]

这两个切片的地址不一样,所以可以确认切片在函数间传递是复制的。而修改一个索引的值后,发现原切片的值也被修改了,说明它们共用一个底层数组。

在函数间传递切片非常高效,而且不需要传递指针和处理复杂的语法,只需要复制切片,然后根据自己的业务修改,最后传递回一个新的切片副本即可,这也是为什么函数间传递参数,使用切片,而不是数组的原因。

string 与 slice

string 底层是一个 byte 的数组,有也可以进行切片操作。

  1. str := "hello world"
  2. s1 := str[0:5]
  3. fmt.Println(s1)

利用切片对数组排序

  1. package main
  2. import (
  3. "fmt"
  4. "sort"
  5. )
  6. func testInts() {
  7. a := [...]int{11,4,2,3,9,20}
  8. sort.Ints(a[:])
  9. fmt.Println(a)
  10. }
  11. func testStrings() {
  12. s := [...]string{"ac", "a", "ab", "AC", "A", "AB"}
  13. sort.Strings(s[:])
  14. fmt.Println(s)
  15. }
  16. func testFload64s() {
  17. f := [...]float64{3.1415, 2.713, 1.024, 1024}
  18. sort.Float64s(f[:])
  19. fmt.Println(f)
  20. }
  21. func main() {
  22. testInts()
  23. testStrings()
  24. testFload64s()
  25. }

查找索引

  1. sort.SearchInts(a []int, b int) 从数组 a 中查找 b 的索引,前提是 a 已经排序
  2. sort.SearchFloats(a []int, b int) 从数组 a 中查找 b 的索引,前提是 a 已经排序
  3. sort.SearchStrings(a []int, b int) 从数组 a 中查找 b 的索引,前提是 a 已经排序

nil切片和空切片???

它们的长度和容量都是0,但是它们指向底层数组的指针不一样。

  • nil切片意味着指向底层数组的指针为nil,表示不存在的切片;
  • 空切片对应的指针是个地址,空切片表示一个空集合。
  1. //nil切片
  2. var nilSlice []int
  3. //空切片
  4. slice := []int{}

slice 只能和空比较