切片是一个动态数组,因为数组的长度是固定的,所以操作起来很不方便,比如一个 names 数组,我想增加一个学生的姓名都没有办法,十分不灵活。所以在开发中数组并不常用,切片类型才是大量使用的。

1、生成切片的方式

1.1、从数组或者切片上切

切片语法:

  1. slice [start : end]
  2. /*
  3. 切片的概念和数组息息相关,切片有三个要素
  4. slice : 表示目标切片对象。
  5. start: 对应目标切片对象的开始索引。
  6. end: 对应目标切片的结束索引。
  7. */
  1. // 定义数组
  2. names := [...]string{"sbh", "sgg", "xhz", "xnq", "cxf"}
  3. // 从数组中生成
  4. s1 := names[0:3]
  5. // 从切片中生成
  6. s2 := s1[2:]
  7. fmt.Println(s1, reflect.TypeOf(s1)) // [sbh sgg xhz] []string (切片类型)
  8. fmt.Println(s2, reflect.TypeOf(s2)) // [xhz] []string
  1. 取出的元素数量为:结束位置-开始位置。
  2. 取出元素不包含结束位置的索引,切出最后一个元素使用:slice[len(slice)] 获取。
  3. 当缺省开始位置时,表示从连续的区域开头到结束位置。
  4. 当缺省结束位置时,表示从开始位置到整个连续区域的末尾。
  5. 两者同事缺省时,与切片本身等效。
  6. 两者同时为 0 时,等效于空切片,一般用于切片复位。

1.2、直接声明切片

除了可以从原有的数组或者切片中生成切片,也可以声明一个新的切片,每一种类型都可以拥有其切片类型,表示多个相同类型元素的连续集合,因此切片类型也可以被声明,切片类型声明格式如下:

  1. var names []Type

其中 names 表示切片的变量名,Type 表示切片对应的元素类型。

  1. names := []string{"sbh", "sgg", "xhz", "xnq", "cxf"}
  2. fmt.Println(names, reflect.TypeOf(names)) // [sbh sgg xhz xnq cxf] []string

2、值类型和引用类型

数据类型从存储方式分为两类:值类型和引用类型!

2.1、值类型

基本数据类型(int、float、bool、string)以及 数组 和 struct 都属于值类型。
特点:变量直接存储值,内存中通常在栈中分配,栈在函数调用完成。值类型变量声明后,不管是否已经赋值,编译器为其分配内存,此时该值存储于栈上。

  1. var a int // int 类型默认值 0
  2. var b string // string 类型默认值为 nil 空
  3. var c bool // bool 类型默认值false
  4. var d [2]int // 数组默认值为[0 0]

当使用等号 = 将一个变量的值赋给另外一个变量时,如 j = i ,实际上是在内存中 将 i 的值进行了 拷贝,可以通过 &i 获取变量 i 的内存地址。此时如果修改某个变量的值,不会影响另一个。

  1. // 整型赋值
  2. a := 10
  3. b := a
  4. fmt.Println(a, &a) // 10 0xc000094000
  5. fmt.Println(b, &b) // 10 0xc000094008
  6. b = 100
  7. fmt.Println(a, &a) // 10 0xc000094000
  8. fmt.Println(b, &b) // 100 0xc000094008
  9. // 数组赋值
  10. c := [3]int{11, 22, 33}
  11. d := c
  12. d[1] = 100
  13. fmt.Printf("%v, %p", c, &c) // [11 22 33], 0xc00001c090
  14. fmt.Printf("%v, %p", d, &d) // [11 100 33], 0xc00001c0a8

2.2、引用类型

指正,slice,map,chan,interface 等都是引用类型
特点:变量存储的是一个地址,这个地址存储最终的值。内存通常在堆上分配,通过GC回收。
变量直接存放的就是一个内存地址值,这个地址值指向的空间的存的才是值。所以修改其中的一个,另外一个也会修改(同一个内存地址)。
引用类型必须申请内存才可以使用,make() 是给引用类型申请内存空间。

  1. // 定义切片类型
  2. var a = []int{1, 2, 3}
  3. b := a // b 和 a的内存地址是一样的
  4. a[0] = 100
  5. fmt.Println(b) // [100 2 3]

3、切片原理

为什么切片 b 会受到切片 a 的影响? 根本原因是切片是引用类型 !
接下来我们通过切片的存储原理来理解引用类型!
切片的构造根本是一个具体数组通过切片起始指针,切片长度以及最大容量三个参数确定下来的。

  1. type Slice struct {
  2. Data uintptr // 指针,指向底层数组中切片指定的开始位置
  3. Len int // 长度,即切片的长度
  4. Cap int // 最大长度(容量),也就是切片的开始位置到数组的最后位置的长度
  5. }

举例子:

  1. //案例:
  2. slice := []int{1, 2, 3, 4, 5}
  3. newSlice := slice[1:3]
  4. fmt.Println(slice)
  5. fmt.Println(len(slice))
  6. fmt.Println(cap(slice))
  7. fmt.Println(newSlice)
  8. fmt.Println(len(newSlice))
  9. fmt.Println(cap(newSlice))
  10. // 地址打印
  11. // a记录的是顺序表的表头,即数组指针 :
  12. // 数组指针 长度 容量,所以打印a地址即打印a第一个表头地址。
  13. fmt.Printf("%p\n", slice)
  14. fmt.Printf("%p\n", &slice)
  15. fmt.Printf("%p\n", &slice[0])
  16. fmt.Printf("%p\n", &slice[1])
  17. fmt.Printf("%p\n", &slice[2])

逻辑如图:
5.3、切片(slice) - 图1

以上图能看出来,很明显,我们操作的是同一块内存地址。

4、make函数

变量的声明我们可以通过var 关键字,然后就可以在程序中使用。当我们不指定变量的默认值时,这些变量的默认值是他们的零值,比如int类型的零值是0,string类型的零值是””,引用类型的零值是nil。
对于例子中的两种类型的声明,我们可以直接使用,对其进行赋值输出。但是我们如果换成引用类型呢?

  1. var arr []int // 如果是 var arr [2]int
  2. arr[0] = 100
  3. fmt.Println(arr)

从这个提示中可以看出,对于引用类型的变量,我们不光要声明它,还要为它分配内存空间。
对于值类型的声明不需要,是因为已经默认帮我们分配好了。要分配内存,就引出来今天的make函数。make也是用于 chan、map 以及 切片的 内存创建,而且它返回的类型就是这 三个类型本身。
如果需要动态的创建一个切片,可以使用 make() 内建函数,格式如下:

  1. make([]Type, size, cap)

其中Type是指切片的元素类型,size 指的是为了这个类型分配多少个元素,cap为预分配的元素数量,这个值设定后不影响size,只是能提前分配空间,降低多次分配空间造成的性能问题。示例如下:

  1. a := make([]int, 2) // 容量(cap)默认跟长度(len)一样
  2. b := make([]int, 2, 10)
  3. fmt.Println(a, b) // [0 0] [0 0]
  4. fmt.Println(len(a), len(b)) // 2 2
  5. fmt.Println(cap(a), cap(b)) // 2 10

使用make()函数生成的切片一定发生了内存分配的操作,但给定开始与结束位置(包括切片复位)的切片只是将新的切片结构指向已经分配好的内存区域,设定开始与结束位置,不会发生内存分配操作。

  1. a := make([]int, 5)
  2. b := a[:3]
  3. a[0] = 100
  4. fmt.Println(a) // [100 0 0 0 0]
  5. fmt.Println(b) // [100 0 0]

5、append扩容(重点)

上面我们已经讲过,切片作为一个动态数组是可以添加元素的,添加方式为内建方法append。

  1. var s []int
  2. // 追加一个空切片的一个值
  3. s1 := append(s, 0)
  4. fmt.Println(s1) // [0]
  5. //可以追加多个值
  6. s2 := append(s, 1, 2, 3, 4)
  7. fmt.Println(s2) // [1 2 3 4]
  8. //可以追加一个切片
  9. s3 := append(s, []int{4, 5, 6}...)
  10. fmt.Println(s3) // [4 5 6]

  1. //案例1
  2. a := []int{1, 2, 3}
  3. fmt.Println(len(a)) //3
  4. fmt.Println(cap(a)) //3
  5. c := append(a, 45)
  6. fmt.Println(a) //[1 2 3]
  7. fmt.Println(c) //[1 2 3 45]
  8. // 案例2
  9. a := make([]int, 3, 10)
  10. fmt.Println(a) // [0 0 0]
  11. b := append(a, 45, 46)
  12. fmt.Println(b) // [0 0 0 45 46]
  13. a[0] = 100
  14. fmt.Println(a) // [100 0 0]
  15. fmt.Println(b) // [100 0 0 45 46]
  16. // 案例3
  17. l := make([]int, 5, 10)
  18. v1 := append(l, 1)
  19. fmt.Println(v1) //[0 0 0 0 0 1]
  20. fmt.Printf("%p\n", &v1) //0xc00000c030
  21. v2 := append(l, 2)
  22. fmt.Println(v2) // [0 0 0 0 0 2]
  23. fmt.Printf("%p\n", &v2) // 0xc00000c060
  24. fmt.Println(v1) //[0 0 0 0 0 2]
  1. 每次append操作都会检查 slice 是否有足够的容量,如果足够会直接在原始数组上追加元素并返回一个新的 slice,底层数组不变,但是这种情况非常危险,极度容易产生bug!而若容量不够,会创建一个新的容量足够的底层数组,先将之前数组的元素赋值过来,再将新元素追加到后面,然后返回新的slice,底层数组改变而这里对新数组的进行扩容。
  2. 扩容策略:如果切片的容量小于 1024个元素,于是扩容的时候就翻倍增加容量。上面的例子也验证了这一情况,总容量从原来的4个翻倍到现在的8个。一旦元素个数超过1024个元素,那么增长因子变成1.25,即每次增加原来容量的四分之一。

5.3、切片(slice) - 图2
经典面试题:

  1. arr := [4]int{10, 20, 30, 40}
  2. s1 := arr[0:2]
  3. s2 := s1
  4. s3 := append(append(append(s1, 1),2),3)
  5. s1[0] = 1000
  6. fmt.Println(s2[0]) // 1000
  7. fmt.Println(s3[0]) // 10

5.3、切片(slice) - 图3

6、插入删除元素

6.1、开头添加元素

  1. var a = []int{1, 2, 3}
  2. a = append([]int{0}, a...) // 在开头添加1个元素
  3. a = append([]int{4, 5, 6}, a...) // 在开头添加1个切片

在切片头添加元素一般都会导致内存的重新分配,而且会导致已有元素全部被复制1次,因此,从切片的开头添加元素的性能要比从尾部追加元素的性能差的多。

6.2、任意位置插入元素

  1. var a []int
  2. a = append(a[:i], append([]int{x},a[i:]...)...)

每个添加操作的第二个append 调用都会创建一个临时切片,并将a[i:]的内容复制到新创建的切片中,然后将临时创建的切片再追加到 a[:i] 中。
思考这样写可以不:

  1. var a = []int{1,2,3,4}
  2. s1:=a[:2]
  3. s2:=a[2:]
  4. fmt.Println(append(append(s1,100,),s2...))

6.3、删除元素

Go语言中并没有删除切片元素的专用方法,我们可以使用切片本身的特性删除元素。

  1. // 从切片中删除元素
  2. a := []int{1, 2, 3, 4, 5}
  3. // 删除为索引为 2 的元素
  4. a = append(a[:2], a[3:]...)
  5. fmt.Println(a)

要从切片a中删除索引为 index 的元素,操作方法: a := append(a[:index],a[index+1:]…)

思考题:

  1. a:=[...]int{1,2,3}
  2. b:=a[:]
  3. b =append(b[:1],b[2:]...)
  4. fmt.Println(a)
  5. fmt.Println(b)

7、切片元素排序

  1. a := []int{1, 2, 3, 4, 5}
  2. sort.Ints(a) // 顺序排序
  3. fmt.Println(a)
  4. sort.Sort(sort.Reverse(sort.IntSlice(a[:]))) // 倒叙排序
  5. fmt.Println(a)