为什么用切片?

  1. 数组的容量固定,不能自动拓展。
    2. 值传递。 数组作为函数参数时,将整个数组值拷贝一份给形参。

在Go语言当,我们几乎可以在所有的场景中,,使用 切片替换数组使用,数据结构是切片,即动态数组,其长度并不固定。

我们可以向切片中追加元素,它会在容量不足时自动扩容。

切片的本质

不是一个数组的指针,是一种数据结构体,用来操作数组内部元素。

https://github.com/golang/go/blob/3b2a578166/src/runtime/slice.go

  1. type slice struct {
  2. array unsafe.Pointer
  3. len int
  4. cap int
  5. }

切片数据结构

Go中的slice依赖于数组,它的底层就是数组,所以数组具有的优点,slice都有。

且slice支持可以通过append向slice中追加元素,长度不够时会动态扩展,通过再次slice切片,可以得到得到更小的slice结构,可以迭代、遍历等。

编译期间的切片是 cmd/compile/internal/types.Slice 类型的,但是在运行时切片可以由如下的 reflect.SliceHeader 结构体表示,其中:

  • Data 是指向数组的指针;
  • Len 是当前切片的长度;
  • Cap 是当前切片的容量,即 Data 数组的大小:
  1. type SliceHeader struct {
  2. Data uintptr
  3. Len int
  4. Cap int
  5. }

Data 是一片连续的内存空间,这片内存空间可以用于存储切片中的全部元素,数组中的元素只是逻辑上的概念,底层存储其实都是连续的,所以我们可以将切片理解成一片连续的内存空间加上长度与容量的标识。

  1. slice := []int{10, 20, 30, 0, 0}

image.png

声明

在 Go 语言中,切片类型的声明方式与数组有一些相似,不过由于切片的长度是动态的,所以声明时只需要指定切片中的元素类型:

  1. []int
  2. []interface{}

数组和切片的声明方式区别

使用字面量来声明切片时,其语法与使用字面量声明数组非常相似。
二者的区别是:

  • [] 运算符里指定值,创建数组
  • [] 中不指定值,创建切片。

    1. myArray := [3]int{10, 20, 30} // 创建有 3 个元素的整型数组
    2. myArray := [...]int{10, 20, 30} // 创建有 3 个元素的整型数组 通过初始化元素的个数确定数组长度
    3. mySlice := []int{10, 20, 30} // 创建长度和容量都是 3 的整型切片

    创建切片

    通过 make() 函数创建切片
    通过字面量创建切片

Go 语言中包含三种初始化切片的方式:

  1. make 创建切片
  2. 字面量 创建切片
  3. 从数组创建切片

make 创建切片

make() 比new()函数多一些操作

  • new()函数只会进行内存分配并做默认的赋0初始化
  • make()可以先为底层数组分配好内存,然后从这个底层数组中再额外生成一个slice并初始化。
  • make只能构建slice、map和channel这3种结构的数据对象,因为它们都指向底层数据结构,都需要先为底层数据结构分配好内存并初始化

语法:

  1. func make(Type, size ...IntegerType) Type
  1. Type 切片类型
  2. size 切片长度 ,实际存储元素的数量,只有在长度的范围内才会初始化零值
  3. 第三个参数为容量,容量是指最多能存储多少个数据

注意事项:

  1. 长度必须<=容量
  2. 通过下标只能读写长度范围内的数据
  3. 只能通过append向切片追加数据 ```go / 创建一个length和capacity都等于5的slice slice := make([]int,5)

// length=3,capacity=5的slice slice := make([]int,3,5)

  1. <a name="YbEXK"></a>
  2. ## 字面量创建切片
  3. 如果使用字面量的方式创建切片,大部分的工作都会在编译期间完成
  4. ```go
  5. // 创建长度和容量都为4的slice,并初始化赋值
  6. colorSlice := []string{"red","blue","black","green"}
  7. // 创建长度和容量为100的slice,并为第100个元素赋值为3
  8. slice := []int{99:3}

从数组创建切片

语法

  1. var sliceName = arr[index1:index2]

参数

参数 描述
var 定义切片变量使用的关键字
sliceName 切片变量名
arr 数组名
index1 数组的开始索引
index2 数组的结束索引

说明
创建一个切片 sliceName,该切片元素的内容是从数组 arr 的索引 index1 开始到索引 index2 结束。

  1. //array直接就是slice的底层数组
  2. array := [...]int{1,2,3}
  3. slice := array[start:end] //左闭右开, array[start]~array[end-1]
  4. slice := array[start:] //array[start]~最后一个元素
  5. slice := array[:end] //第一个元素~array[end-1]
  6. slice := array[:] //全部的array元素
  • 切片长度: 切片对应数组片段的长度
  • 切片容量: 切片第一个元素在数组中的起始位置到数组最后一个元素的长度

切片的长度(len) & 容量(cap) & 遍历

长度 & 容量

  • len n := len(slice)
  • cat n := cap(slice)

遍历

for

  1. for i := 0; i < len(slice); i++ { //slice[i] }

通过 len 函数,获取切片元素的个数,然后通过 for 循环加索引的形式获取每一个切片元素的值。

for range

通过 for range 的形式来遍历切片元素:

  • index 即是切片的索引
  • value 是切片的索引 index 处对应的切片的值

如果我们不需要索引或者值,可以通过 _ 的形式忽略。

  1. for index, value := range sliceHaiCoder{
  2. }
  3. for _, value := range sliceHaiCoder{
  4. }
  1. package main
  2. import (
  3. "fmt"
  4. "reflect"
  5. )
  6. func main() {
  7. color := []string{"red","blue","black","green"}
  8. fmt.Println("color:", color)
  9. fmt.Printf("len = %d, cap = %d\n", len(color), cap(color))
  10. // 遍历切片 空切片 不会有任元素
  11. for i := 0; i < len(color); i++ {
  12. fmt.Printf("color[%d] = %q\n", i, color[i])
  13. }
  14. // 获取切片类型
  15. fmt.Println("color type =", reflect.TypeOf(color))
  16. /**
  17. color: [red blue black green]
  18. len = 4, cap = 4
  19. color[0] = "red"
  20. color[1] = "blue"
  21. color[2] = "black"
  22. color[3] = "green"
  23. color type = []string
  24. */
  25. list := []int{}
  26. fmt.Println("list:", list)
  27. fmt.Printf("len = %d, cap = %d\n", len(list), cap(list))
  28. // 遍历切片 空切片不会有任何打印,空数组会打印类型的默认值
  29. for index,value := range list {
  30. fmt.Printf("list[%d] = %d\n", index, value)
  31. }
  32. /**
  33. list: []
  34. len = 0, cap = 0
  35. */
  36. list2 := []int{4:100}
  37. fmt.Println("list2:", list2)
  38. fmt.Printf("len = %d, cap = %d\n", len(list2), cap(list2))
  39. // 遍历切片
  40. for index,value := range list2 {
  41. fmt.Printf("list2[%d] = %d\n", index, value)
  42. }
  43. /**
  44. list2: [0 0 0 0 100]
  45. len = 5, cap = 5
  46. list2[0] = 0
  47. list2[1] = 0
  48. list2[2] = 0
  49. list2[3] = 0
  50. list2[4] = 100
  51. */
  52. }

切片的截取

通过切片创建新的切片

  1. slice[start:end]
  2. slice[start:end:max]
  • start 表示从 slice 的第几个元素开始切
  • end 控制切片的长度 len = end-start
  • max 控制切片的容量 cap = max-start 如果没有给定 max,则表示切到底层数组的最尾部

下面是几种常见的简写形式:

  1. slice[start:] // 从 start 切到最尾部
  2. slice[:end] // 从最开头切到 end(不包含 end)
  3. slice[:] // 从头切到尾,等价于复制整个 slice
  1. // 创建一个整型切片
  2. // 其长度和容量都是 5 个元素
  3. myNum := []int{10, 20, 30, 40, 50}
  4. // 创建一个新切片
  5. // 其长度为 2 个元素 len=3-1,容量为 4 个元素 cap=5-1
  6. newNum := myNum[1:3]
  1. package main
  2. import "fmt"
  3. func main() {
  4. // 整切切片 array
  5. array := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
  6. // 复制整个切片
  7. slice1 := array[:]
  8. fmt.Println("slice1 = ", slice1)
  9. fmt.Printf("slice1 len = %d, slice1 cap = %d\n", len(slice1), cap(slice1))
  10. /**
  11. slice1 = [0 1 2 3 4 5 6 7 8 9]
  12. slice1 len = 10, slice1 cap = 10
  13. */
  14. // 从下标第2个位置开始截取所有元素
  15. slice2 := array[2:]
  16. fmt.Println("slice2 = ", slice2)
  17. fmt.Printf("slice2 len = %d, slice2 cap = %d\n", len(slice2), cap(slice2))
  18. /**
  19. slice2 = [2 3 4 5 6 7 8 9]
  20. slice2 len = 8, slice2 cap = 8
  21. */
  22. // 从下标0开始,截取到第2个元素
  23. // len=2-0 cap=10-0
  24. slice3 := array[:2]
  25. fmt.Println("slice3 = ", slice3)
  26. fmt.Printf("slice3 len = %d, slice3 cap = %d\n", len(slice3), cap(slice3))
  27. /**
  28. slice3 = [0 1]
  29. slice3 len = 2, slice3 cap = 10
  30. */
  31. //a[3], a[4], a[5] len = 6-3=3 cap = 7-3=4
  32. slice4 := array[3:6:7]
  33. fmt.Println("slice4 = ", slice4)
  34. fmt.Printf("slice4 len = %d, slice4 cap = %d\n", len(slice4), cap(slice4))
  35. /*
  36. slice4 = [3 4 5]
  37. slice4 len = 3, slice4 cap = 4
  38. */
  39. // len=3-1 cap=10-1
  40. slice5 := array[1:3]
  41. fmt.Println("slice5 = ", slice5)
  42. fmt.Printf("slice5 len = %d, slice5 cap = %d\n", len(slice5), cap(slice5))
  43. /**
  44. slice5 = [1 2]
  45. slice5 len = 2, slice5 cap = 9
  46. */
  47. }

切片访问

  1. // 访问单个切片元素
  2. sliceName[index] // 获取切片 sliceName 的索引为 index 处的元素
  3. //索引切片获取切片元素
  4. sliceName[start:end] // index1 开始索引 结束索引
  1. package main
  2. import "fmt"
  3. func main() {
  4. //使用索引的形式,访问单个切片元素
  5. var goFrameList = []string{"gin", "go-frame", "beego"}
  6. for i:=0; i<len(goFrameList); i++ {
  7. // slice[0] slice[1] slice[2]
  8. fmt.Printf("slice[%d]=%q\n", i, goFrameList[i])
  9. }
  10. /**
  11. slice[0]="gin"
  12. slice[1]="go-frame"
  13. slice[2]="beego"
  14. */
  15. // 索引切片获取切片元素
  16. list := goFrameList[0:2]
  17. fmt.Println("list =", list)
  18. /**
  19. list = [gin go-frame]
  20. */
  21. }

添加 删除 复制

append函数详解

语法

  1. func append(slice []Type, elems ...Type) []Type

参数

参数 描述
slice 要追加元素的切片。
elems 要追加的元素列表。

返回值

返回追加元素之后的切片。

说明

append 函数的

  1. 第一个参数是需要被添加元素的切片
  2. 第二个参数是一个 可变参数,传入任意个数的切片的元素
  3. 最后返回一个新的切片

添加

  1. package main
  2. import "fmt"
  3. func main() {
  4. goFrameList := []string{"Gin", "Go-frame", "Beego"}
  5. goFrameList = append(goFrameList, "Martini")
  6. fmt.Println("goFrameList", goFrameList) //goFrameList [Gin Go-frame Beego Martini]
  7. // 添加多个元素
  8. goFrameList = append(goFrameList, "Iris", "Echo")
  9. fmt.Println("goFrameList", goFrameList) //goFrameList [Gin Go-frame Beego Martini Iris Echo]
  10. //添加一个切片 相当于尾部添加
  11. // append 函数的第二个参数是一个可变参数,要在追加的切片 appendSlice 末尾加上 …
  12. goFrameList = append(goFrameList, []string{"ueumd", "yuque"}...)
  13. fmt.Println("goFrameList", goFrameList) //goFrameList [Gin Go-frame Beego Martini Iris Echo ueumd yuque]
  14. // 在切片头部添加元素
  15. goFrameList = append([]string{"Golang"}, goFrameList...)
  16. fmt.Println("goFrameList", goFrameList)
  17. // 在index处插入切片
  18. js := []string{"Vue","React"}
  19. goFrameList = append(goFrameList[:1], append(js, goFrameList[1:]...)...)
  20. fmt.Println("goFrameList", goFrameList)
  21. /**
  22. goFrameList [Gin Go-frame Beego Martini]
  23. goFrameList [Gin Go-frame Beego Martini Iris Echo]
  24. goFrameList [Gin Go-frame Beego Martini Iris Echo ueumd yuque]
  25. goFrameList [Golang Gin Go-frame Beego Martini Iris Echo ueumd yuque]
  26. goFrameList [Golang Vue React Gin Go-frame Beego Martini Iris Echo ueumd yuque]
  27. */
  28. }

删除

Go 语言 的 切片 删除元素使用 append 函数 来间接的实现。

删除索引 index 处的元素

  1. slice = append(slice[:index], slice[index+1:]...)

删除索引 start 到 end 处的元素

  1. slice = append(slice[:start], slice[end:]...)
  1. package main
  2. import "fmt"
  3. func main() {
  4. goFrameList := []string{"Gin", "Go-frame", "Beego", "Martini", " Iris", "Echo"}
  5. // 删除索引 index 处的元素 Go-frame
  6. goFrameList = append(goFrameList[:1], goFrameList[1+1:]...)
  7. fmt.Println("goFrameList", goFrameList) // goFrameList [Gin Beego Martini Iris Echo]
  8. // 删除索引 start 到 end 处的元素 删除 Beego Martini
  9. goFrameList = append(goFrameList[:1], goFrameList[3:]...)
  10. fmt.Println("goFrameList", goFrameList) //goFrameList [Gin Iris Echo]
  11. /**
  12. goFrameList [Gin Beego Martini Iris Echo]
  13. goFrameList [Gin Iris Echo]
  14. */
  15. }

切片作为函数参数

前面说过,虽然slice实际上包含了3个属性,它的数据结构类似于[3/5]0xc42003df10,但仍可以将slice看作一种指针。这个特性直接体现在函数参数传值上。

Go中函数的参数是按值传递的,所以调用函数时会复制一个参数的副本传递给函数。

如果传递给函数的是slice,它将复制该slice副本给函数,这个副本实际上就是[3/5]0xc42003df10,所以传递给函数的副本仍然指向源slice的底层数组。

  1. package main
  2. import "fmt"
  3. func main() {
  4. slice := []int{1, 2, 3, 4, 5}
  5. fmt.Println("slice:", slice)
  6. fmt.Printf("&slice: %p\n", &slice)
  7. fmt.Println("\n------修改后-----\n")
  8. modifySlice(&slice)
  9. fmt.Println("slice:", slice)
  10. add(slice)
  11. fmt.Println("slice:", slice)
  12. /**
  13. slice: [1 2 3 4 5]
  14. &slice: 0xc000194000
  15. ------修改后-----
  16. &slice: 0xc00018a020
  17. slice: [1 200 3 4 5]
  18. slice: [2 201 4 5 6]
  19. */
  20. }
  21. func modifySlice(slice *[]int) {
  22. fmt.Printf("&slice: %p\n", &slice)
  23. (*slice)[1] = 200
  24. }
  25. func add(s []int) {
  26. for index, _ := range s {
  27. s[index] += 1
  28. }
  29. }

打印结果里看出:

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

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

切片扩容

append函数会智能的增长底层数组的容量。目前的算法是:

  • 容量小于1000个时,总是成倍的增长
  • 容量超过1000个,增长因子设为1.25,也就是说每次会增加25%的容量

image.png

  1. package main
  2. import "fmt"
  3. func main() {
  4. //如果超过原来的容量,通常以2倍容量扩容
  5. slice := make([]int, 0, 1) //容量为1
  6. oldCap := cap(slice)
  7. for i := 0; i < 20; i++ {
  8. slice = append(slice, i)
  9. if newCap := cap(slice); oldCap < newCap {
  10. fmt.Printf("cap: %d ===> %d\n", oldCap, newCap)
  11. oldCap = newCap
  12. }
  13. }
  14. }
  15. /*
  16. cap: 1 ===> 2
  17. cap: 2 ===> 4
  18. cap: 4 ===> 8
  19. cap: 8 ===> 16
  20. cap: 16 ===> 32
  21. */

练习代码

  1. package main
  2. import (
  3. "fmt"
  4. "unsafe"
  5. )
  6. type slice struct {
  7. array unsafe.Pointer // 指向数组的指针
  8. len int // 切片中元素数量
  9. cap int // array 数组的总容量
  10. }
  11. // 深度拷贝: copy(sliceA, sliceB)
  12. // 浅拷贝: sliceA = sliceB
  13. // 1. 通过数组创建切片 array[startIndex:endIndex]
  14. func createSliceByArray() {
  15. var arr = [5]int{1, 3, 5, 7, 9}
  16. // 从数组0下标开始取,一直取到2下标前面一个索引
  17. var sce1 = arr[0:2]
  18. fmt.Println(sce1) //[1 3]
  19. // 切片len = 结束位置 - 开始位置
  20. fmt.Println(len(sce1)) //2 2-0 = 2
  21. // 切片cap = 数组长度 - 开始位置
  22. fmt.Println(cap(sce1)) //5 5-0=5
  23. fmt.Printf("%p\n", &arr)
  24. fmt.Printf("%p\n", &arr[0])
  25. // 切片底层是指针数组,指向的是一块内存地址,前面无须加地址符号 &
  26. fmt.Printf("%p\n", sce1)
  27. }
  28. // 2. 通过make函数创建 make([]类型, 长度, 容量)
  29. func createSliceByMake() {
  30. var sce = make([]int, 3, 5)
  31. fmt.Println(sce) //[0 0 0]
  32. }
  33. // 3. Go语法糖快速创建
  34. // 3.1 不能指长度
  35. // 3.2 长度和容量相等
  36. func createSliceByQuickly() {
  37. // 这是数组 暗示长度
  38. var arr = [...]int{1, 3, 5}
  39. fmt.Println(arr)
  40. // 这是切片
  41. var sce = []int{1, 3, 5}
  42. fmt.Println(sce) // [1 3 5]
  43. fmt.Println(len(sce), cap(sce)) // 3 3
  44. }
  45. /**
  46. append函数会在切片末尾添加一个元素, 并返回一个追加数据之后的切片
  47. 利用append函数追加数据时,如果追加之后没有超出切片的容量,那么返回原来的切片, 如果追加之后超出了切片的容量,那么返回一个新的切片
  48. append函数每次给切片扩容都会按照原有切片容量*2的方式扩容
  49. 如果希望切片自动扩容,那么添加数据时必须使用append方法
  50. 和数组一样, 如果通过切片名称[索引]方式操作切片, 不能越界
  51. 切片的基本使用方式和数组一样, 可以通过切片名称[索引]方式操作切片
  52. */
  53. func appendEle() {
  54. sce := []int{0}
  55. sce[0] = 1
  56. fmt.Println(sce)
  57. // append函数会在切片末尾添加一个元素, 并返回一个追加数据之后的切片
  58. sce = append(sce, 200, 300, 400) // [1 200 300 400]
  59. fmt.Println(sce)
  60. }
  61. func init() {
  62. // createSliceByArray()
  63. // createSliceByMake()
  64. // createSliceByQuickly()
  65. }
  66. type ConfigBanWord struct {
  67. Id int
  68. Txt string
  69. }
  70. // 这是一个切片 相当于一个指针数组,但不用初始化
  71. var configBanWordSlice []*ConfigBanWord
  72. // 这是一个指针数组 编译错误
  73. // var configBanWordArray [...]*ConfigBanWord
  74. // 只能这样定义,要初始化
  75. var configBanWordArray = [...]*ConfigBanWord {&ConfigBanWord{Id: 1, Txt: "外挂"}, &ConfigBanWord{Id: 5, Txt: "赚钱"}}
  76. func demo() {
  77. configBanWordSlice = append(configBanWordSlice,
  78. &ConfigBanWord{Id: 1, Txt: "外挂"},
  79. &ConfigBanWord{Id: 2, Txt: "辅助"},
  80. &ConfigBanWord{Id: 3, Txt: "微信"},
  81. &ConfigBanWord{Id: 4, Txt: "代练"},
  82. &ConfigBanWord{Id: 5, Txt: "赚钱"},
  83. )
  84. for index, data := range configBanWordSlice {
  85. fmt.Println(index, data.Txt)
  86. /**
  87. 0 外挂
  88. 1 辅助
  89. 2 微信
  90. 3 代练
  91. 4 赚钱
  92. */
  93. }
  94. for index, data := range configBanWordArray {
  95. /**
  96. 0 外挂
  97. 1 赚钱
  98. */
  99. fmt.Println(index, data.Txt)
  100. }
  101. }
  102. func main() {
  103. appendEle()
  104. demo()
  105. }

参考:
https://www.cnblogs.com/f-ck-need-u/p/9854932.html
https://www.jianshu.com/p/354fce23b4f0
https://draveness.me/golang/docs/part2-foundation/ch03-datastructure/golang-array-and-slice/