Go 语言的切片(slice)是对数组的抽象概念、是一种比较特殊的数据结构,这种数据结构更便于使用和管理数据集合 Go 数组的长度不可改变,在特定场景中这样的集合就不太适用,Go中提供了一种灵活,功能强悍的内置类型切片(“动态数组”),与数组相比切片的长度是不固定的,可以追加元素,在追加时可能使切片的容量增大 切片是围绕动态数组的概念构建的,可以按需自动增长和缩小。切片的动态增长是通过内置函数 append() 来实现的,这个函数可以快速且高效地增长切片,也可以通过对切片再次切割,缩小一个切片的大小。因为切片的底层也是在连续的内存块中分配的,所以切片还能获得索引、迭代以及为垃圾回收优化的好处

切片的内部实现

切片是一个很小的对象,它对底层的数组(内部是通过数组保存数据的)进行了抽象,并提供相关的操作方法。切片是一个有三个字段的数据结构,这些数据结构包含 Golang 需要操作底层数组的元数据

  1. package main
  2. import "fmt"
  3. func main() {
  4. var slice []int = make([]int, 3, 5)
  5. // arr := []int{10,20,30}
  6. // copy(slice, arr)
  7. slice[0] = 10
  8. slice[1] = 20
  9. slice[2] = 30
  10. fmt.Printf("len=%d cap=%d slice=%v",len(slice),cap(slice),slice)
  11. }
  12. /*
  13. len=3 cap=5 slice=[10 20 30]
  14. */

image.png

  • slice的确是一个引用类型
  • slice从底层来说,其实是个数据结构(struct结构体)
    1. type slice struct{
    2. ptr *[2]int //指针
    3. len int //长度
    4. cap int //容量
    5. }

切片的定义、创建、初始化

1. 定义切片

你可以声明一个未指定大小的数组来定义切片,切片可以不需要说明长度:

  1. var identifier []T


2. 通过 make() 函数创建切片

  1. var slice []T = make([]T, len) //定义后赋值
  2. slice := make([]T, len) //简写
  3. //简单的示例
  4. slice := make([]int, 5)

也可以指定可选参数容量capacity

  1. slice := make([]T, len, capacity)
  2. //简单示例:创建一个整型切片,其长度为 3 个元素,容量为 5 个元素
  3. slice := make([]int, 3, 5)

⚠️ 这里 len 是数组的长度并且也是切片的初始长度,Golang 不允许创建容量小于长度的切片,当创建的切片容量小于长度时会在编译时刻报错: len larger than cap in make([]int)

3. 通过字面量创建切片

直接初始化切片,[]int表示是int的切片类型,{1,2,3}为初始化值,其cap=len=3

  1. // 创建字符串切片,其长度和容量都是 3 个元素
  2. myStr := []string{"Jack", "Mark", "Nick"}
  3. // 创建一个整型切片,其长度和容量都是 4 个元素
  4. myNum := []int{10, 20, 30, 40}

当使用切片字面量创建切片时,还可以设置初始长度和容量。要做的就是在初始化时给出所需的长度和容量作为索引。下面的语法展示了如何使用索引方式创建长度和容量都是100个元素的切片:

  1. // 创建字符串切片,使用空字符串初始化第 100 个元素
  2. myStr := []string{99: ""}

区分数组与切片的声明方式

当使用字面量来声明切片时,其语法与使用字面量声明数组非常相似。二者的区别是:如果在 [] 运算符里指定了一个值,那么创建的就是数组而不是切片。只有在 [] 中不指定值的时候,创建的才是切片。看下面的例子:

  1. //创建有 2 个元素的整型数组
  2. var myArray [2]int
  3. myArray := [2]int{1,2}
  4. // 创建长度和容量都是 3 的整型切片
  5. var mySlice []int
  6. mySlice := []int{10, 20, 30}

切片的使用

初始化切片s,是数组arr的引用

  1. s := arr[:]

将arr中从下标startIndex到endIndex-1 下的元素创建为一个新的切片

  1. s := arr[startIndex:endIndex]

默认 endIndex 时将表示一直到arr的最后一个元素

  1. s := arr[startIndex:]

默认 startIndex 时将表示从arr的第一个元素开始

  1. s := arr[:endIndex]

通过切片s初始化切片s1

  1. s1 := s[startIndex:endIndex]

通过内置函数make()初始化切片s,[]int 标识为其元素类型为int的切片

  1. s :=make([]int,len,cap)

创建新的切片的本质

让我们通过下面的例子来理解通过切片创建新的切片的本质:

  1. package main
  2. import "fmt"
  3. func main() {
  4. // 创建一个整型切片,其长度和容量都是 5 个元素
  5. myNum := []int{10, 20, 30, 40, 50}
  6. // 创建一个新切片,其长度为 2 个元素,容量为 4 个元素
  7. newNum := myNum[1:3]
  8. fmt.Printf("myNum=%v,len(myNum)=%d,cap(myNum)=%d\n",myNum,len(myNum),cap(myNum))
  9. fmt.Printf("newNum=%v,len(newNum)=%d,cap(newNum)=%d\n",newNum,len(newNum),cap(myNum))
  10. }
  11. /*
  12. myNum=[10 20 30 40 50],len(myNum)=5,cap(myNum)=5
  13. newNum=[20 30],len(newNum)=2,cap(newNum)=5
  14. */

执行上面的代码后,我们有了两个切片,它们共享同一段底层数组,但通过不同的切片会看到底层数组的不同部分:
image.png
⚠️注意:截取新切片时的原则是 “左含右不含”

  • newNum 是从 myNum 的 index=1 处开始截取,截取到 index=3 的前一个元素,也就是不包含 index=3 这个元素。所以,新的 newNum 是由 myNum 中的第2个元素、第3个元素组成的新的切片构,长度为 2,容量为 4
  • 切片 myNum 能够看到底层数组全部 5 个元素的容量,而 newNum 能看到的底层数组的容量只有 4 个元素。newNum 无法访问到底层数组的第一个元素。所以,对 newNum 来说,那个元素就是不存在的

共享底层数组的切片

现在两个切片 myNum 和 newNum 共享同一个底层数组。如果一个切片修改了该底层数组的共享

  1. package main
  2. import "fmt"
  3. func main() {
  4. // 创建一个整型切片,其长度和容量都是 5 个元素
  5. myNum := []int{10, 20, 30, 40, 50}
  6. // 创建一个新切片,其长度为 2 个元素,容量为 4 个元素
  7. newNum := myNum[1:3]
  8. newNum[1] = 35
  9. fmt.Println(myNum,"\n",newNum)
  10. }
  11. /*
  12. [10 20 35 40 50]
  13. [20 35]
  14. */

把 35 赋值给 newNum 索引为 1 的元素的同时也是在修改 myNum 索引为 2 的元素:
image.png

切片只能访问到其长度内的元素

上面的代码可以通过编译,但是会产生运行时错误:
panic: runtime error: index out of range

  1. package main
  2. import "fmt"
  3. func main() {
  4. // 创建一个整型切片,其长度和容量都是 5 个元素
  5. myNum := []int{10, 20, 30, 40, 50}
  6. // 创建一个新切片,其长度为 2 个元素,容量为 4 个元素
  7. newNum := myNum[1:3]
  8. // 修改 newNum 索引为 3 的元素,这个元素对于 newNum 来说并不存在
  9. newNum[3] = 45 //panic: runtime error: index out of range [3] with length 2
  10. fmt.Println(myNum,"\n",newNum)
  11. }
  12. /*
  13. [10 20 35 40 50]
  14. [20 35]
  15. */

切片的遍历

  • for 和for range 两种方式 ```go package main import “fmt”

func main() { var arr [5]int = […]int{10, 20, 30, 40, 50} slice := arr[1:] //[20 30 40 50]

  1. for i := 0; i < len(slice); i++ {
  2. fmt.Printf("i=%v v=%v \n", i, slice[i])
  3. }
  4. fmt.Printf("\n—————————————— 分界线 ——————————————\n")
  5. for i, v := range slice {
  6. fmt.Printf("i=%v v=%v \n", i, v)
  7. }

}

/ i=0 v=20 i=1 v=30 i=2 v=40 i=3 v=50 —————————————— 分界线 —————————————— i=0 v=20 i=1 v=30 i=2 v=40 i=3 v=50 /

  1. <a name="g8ma2"></a>
  2. #### len() 和 cap() 函数
  3. 切片是可索引的,并且可以由 len() 方法获取长度<br />切片提供了计算容量的方法 cap() 可以测量切片最长可以达到多少
  4. ```go
  5. package main
  6. import "fmt"
  7. func main() {
  8. var numbers = make([]int,3,5)
  9. printSlice(numbers)
  10. }
  11. func printSlice(x []int){
  12. fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x)
  13. }
  14. /*
  15. 以上实例运行输出结果为:
  16. len=3 cap=5 slice=[0 0 0]
  17. */

空(nil)切片

  1. var myNum []int // 创建 nil 整型切片
  2. myNum := make([]int, 0) // 使用 make 创建空的整型切片
  3. myNum := []int{} // 使用切片字面量创建空的整型切片

image.png
image.png

一个切片在未初始化之前默认为 nil,长度为 0,实例如下:

  1. package main
  2. import "fmt"
  3. func main() {
  4. var numbers []int
  5. if(numbers == nil){
  6. printSlice(numbers)
  7. fmt.Printf("切片是nil的类型的\n")
  8. }
  9. numbers = []int{}
  10. if(numbers != nil){
  11. printSlice(numbers)
  12. fmt.Printf("空切片")
  13. }
  14. }
  15. func printSlice(x []int){
  16. fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x)
  17. }
  18. /*
  19. len=0 cap=0 slice=[]
  20. 切片是nil的类型的
  21. len=0 cap=0 slice=[]
  22. 空切片
  23. */

切片截取

可以通过设置下限及上限来设置截取切片 [lower-bound:upper-bound],实例如下:

  1. package main
  2. import "fmt"
  3. func main() {
  4. // 创建切片
  5. arr := []int{0,1,2,3,4,5,6,7,8}
  6. printSlice(arr)
  7. // 打印原始切片
  8. fmt.Println("arr ==", arr)
  9. // 打印子切片从索引1(包含) 到索引4(不包含
  10. fmt.Println("arr[1:4] ==", arr[1:4])
  11. // 默认下限为
  12. fmt.Println("arr[:3] ==", arr[:3])
  13. // 默认上限为 len(s)
  14. fmt.Println("arr[4:] ==", arr[4:])
  15. arr1 := make([]int,0,5)
  16. printSlice(arr1)
  17. // 打印子切片从索引 0(包含) 到索引 2(不包含)
  18. arr2 := arr[:2]
  19. printSlice(arr2)
  20. // 打印子切片从索引 2(包含) 到索引 5(不包含)
  21. arr3 := arr[2:5]
  22. printSlice(arr3)
  23. }
  24. func printSlice(x []int){
  25. fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x)
  26. }
  27. /*
  28. 执行以上代码输出结果为:
  29. len=9 cap=9 slice=[0 1 2 3 4 5 6 7 8]
  30. arr == [0 1 2 3 4 5 6 7 8]
  31. arr[1:4] == [1 2 3]
  32. arr[:3] == [0 1 2]
  33. arr[4:] == [4 5 6 7 8]
  34. len=0 cap=5 slice=[]
  35. len=2 cap=9 slice=[0 1]
  36. len=3 cap=7 slice=[2 3 4]
  37. */

append() 和 copy() 函数

如果想增加切片的容量,我们必须创建一个新的更大的切片并把原分片的内容都拷贝过来
下面的代码描述了从拷贝切片的 copy 方法和向切片追加新元素的 append 方法

  1. package main
  2. import "fmt"
  3. func main() {
  4. var arr []int
  5. printSlice(arr)
  6. // 允许追加空切片
  7. arr = append(arr, 0)
  8. printSlice(arr)
  9. // 向切片添加一个元素
  10. arr = append(arr, 1)
  11. printSlice(arr)
  12. // 同时添加多个元素
  13. arr = append(arr, 2,3,4)
  14. printSlice(arr)
  15. // 创建切片 arr1 是之前切片的两倍容
  16. arr1 := make([]int, len(arr), (cap(arr))*2)
  17. // 拷贝 arr 的内容到 arr1
  18. copy(arr1,arr)
  19. printSlice(arr1)
  20. }
  21. func printSlice(x []int){
  22. fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x)
  23. }
  24. /*
  25. 以上代码执行输出结果为:
  26. len=0 cap=0 slice=[]
  27. len=1 cap=1 slice=[0]
  28. len=2 cap=2 slice=[0 1]
  29. len=5 cap=6 slice=[0 1 2 3 4]
  30. len=5 cap=12 slice=[0 1 2 3 4]
  31. */

append()的扩容高阶讲解

append是一个内置函数,用来在指定的slice后面添加元素(可以多个),并且返回一个新的slice.我们知道每个slice在底层都有一个数组作为支撑,如果在append元素后,修改改了返回的slice,源数组会发生相对应的改变吗?

  1. package main
  2. import "fmt"
  3. func main() {
  4. //案例1
  5. var myNum []int = []int{10, 20, 30, 40, 50}
  6. newNum := myNum[1:3] // [20 30]
  7. newNum = append(newNum, 60) // [20 30 60]
  8. fmt.Printf("myNum=%v newNum=%v cap(newNum)=%v\n",myNum,newNum,cap(newNum))
  9. //案例2
  10. myNum = []int{10, 20, 30, 40} // 创建一个长度和容量都是 4 的整型切片
  11. newNum = append(myNum, 50) //[10 20 30 40 50]
  12. fmt.Printf("myNum=%v newNum=%v cap(newNum)=%v\n",myNum,newNum,cap(newNum))
  13. //案例3
  14. myNum = []int{10, 20, 30, 40} // 创建一个长度和容量都是 4 的整型切片
  15. newNum = myNum[1:3] //[20 30]
  16. newNum = append(newNum, 1,2,3,4,5) //[10 20 30 40 50 1 2 3 4 5]
  17. fmt.Printf("myNum=%v newNum=%v cap(newNum)=%v\n",myNum,newNum,cap(newNum))
  18. }
  19. /*
  20. myNum=[10 20 30 60 50] newNum=[20 30 60] cap(newNum)=4
  21. myNum=[10 20 30 40] newNum=[10 20 30 40 50] cap(newNum)=8
  22. myNum=[10 20 30 40] newNum=[20 30 1 2 3 4 5] cap(newNum)=8
  23. */

答案 不一定,这要取决slice的底层数组空间。为什么呢?

  • 相对于数组而言,使用切片的一个好处是:可以按需增加切片的容量。
  • Golang 内置的 append() 函数会处理增加长度时的所有操作细节。要使用 append() 函数,需要一个被操作的切片和一个要追加的值,当 append() 函数返回时,会返回一个包含修改结果的新切片。
  • 函数 append() 总是会增加新切片的长度,而容量有可能会改变,也可能不会改变,这取决于被操作的切片的可用容量

image.png

  • 在上述代码案例1中, 此时因为 newNum 在底层数组里还有额外的容量可用,append() 函数将可用的元素合并入切片的长度,并对其进行赋值。由于和原始的切片共享同一个底层数组,myNum 中索引为 3 的元素的值也被改动了

image.png

  • 在上述代码案例2中, 如果切片的底层数组没有足够的可用容量,append() 函数会创建一个新的底层数组,将被引用的现有的值复制到新数组里,再追加新的值,此时 append 操作同时增加切片的长度和容量
  • 函数 append() 会智能地处理底层数组的容量增长。在切片的容量小于 1000 个元素时,总是会成倍地增加容量。一旦元素个数超过 1000,容量的增长因子会设为 1.25,也就是会每次增加 25%的容量(随着语言的演化,这种增长算法可能会有所改变)

限制切片的容量

在创建切片时,使用第三个索引选项引可以用来控制新切片的容量。其目的并不是要增加容量,而是要限制容量。允许限制新切片的容量为底层数组提供了一定的保护,可以更好地控制追加操作

  1. package main
  2. import "fmt"
  3. func main() {
  4. fruit := []string{"Apple", "Orange", "Plum", "Banana", "Grape"} // 创建长度和容量都是 5 的字符串切片
  5. myFruit := fruit[2:3:4] // 将第三个元素切片,并限制容量, 其长度为 1 个元素,容量为 2 个元素
  6. fmt.Printf("fruit=%v len(fruit)=%d cap(fruit)=%d\n",fruit,len(fruit),cap(fruit))
  7. fmt.Printf("myFruit=%v len(myFruit)=%d cap(myFruit)=%d\n",myFruit,len(myFruit),cap(myFruit))
  8. }
  9. /*
  10. fruit=[Apple Orange Plum Banana Grape] myFruit=[Plum]
  11. */

这个切片操作执行后,新切片里从底层数组引用了 1 个元素,容量是 2 个元素。具体来说,新切片引用了 Plum 元素,并将容量扩展到 Banana 元素:
image.png
如果设置的容量比可用的容量还大,就会得到一个运行时错误:

  1. myFruit := fruit[2:3:6]
  2. panic: runtime error: slice bounds out of range

内置函数 append() 在操作切片时会首先使用可用容量。一旦没有可用容量,就会分配一个新的底层数组。这导致很容易忘记切片间正在共享同一个底层数组。一旦发生这种情况,对切片进行修改,很可能会导致随机且奇怪的问题,这种问题一般都很难调查。如果在创建切片时设置切片的容量和长度一样,就可以强制让新切片的第一个 append 操作创建新的底层数组,与原有的底层数组分离。这样就可以安全地进行后续的修改操作了:

  1. package main
  2. import "fmt"
  3. func main() {
  4. fruit := []string{"Apple", "Orange", "Plum", "Banana", "Grape"}
  5. myFruit := fruit[2:3:3]
  6. myFruit = append(myFruit, "Kiwi") // 向 myFruit 追加新字符串
  7. fmt.Printf("myFruit=%v len(myFruit)=%d cap(myFruit)=%d\n",myFruit,len(myFruit),cap(myFruit))
  8. }
  9. /*
  10. myFruit=[Plum Kiwi] len(myFruit)=2 cap(myFruit)=2
  11. */

这里,我们限制了 myFruit 的容量为 1。当我们第一次对 myFruit 调用 append() 函数的时候,会创建一个新的底层数组,这个数组包括 2 个元素,并将水果 Plum 复制进来,再追加新水果 Kiwi,并返回一个引用了这个底层数组的新切片。因为新的切片 myFruit 拥有了自己的底层数组,所以杜绝了可能发生的问题。我们可以继续向新切片里追加水果,而不用担心会不小心修改了其他切片里的水果。可以通过下图来理解此时内存中的数据结构:
image.png

将一个切片追加到另一个切片

内置函数 append() 也是一个可变参数的函数。这意味着可以在一次调用中传递多个值。如果使用 … 运算符,可以将一个切片的所有元素追加到另一个切片里:

  1. package main
  2. import "fmt"
  3. func main() {
  4. // 创建两个切片,并分别用两个整数进行初始化
  5. num1 := []int{1, 2}
  6. num2 := []int{3, 4}
  7. // 将两个切片追加在一起,并显示结果
  8. fmt.Printf("%v\n", append(num1, num2...))
  9. }
  10. /*
  11. [1 2 3 4]
  12. */

切片知识总结

  • 引⽤类型。但⾃⾝是结构体,值拷⻉传递
  • 一般使用make()创建
  • 使用len()获取元素个数,cap()获取容量
    • 属性 len 表⽰可⽤元素数量,读写操作不能超过该限制
    • 属性 cap 表⽰最⼤扩张容量,不能超出数组限制
  • 如果 slice == nil,那么 len、 cap 结果都等于 0
  • 作为变长数组的替代方案,可以关联底层数组的局部或全部
  • 可以直接创建或从底层数组获取生成
  • 如果多个slice指向相同底层数组,其中一个值的改变会影响全部
  • 在通过下标访问元素时下标不能超过len大小,如同数组的下标不能超出len范围一样