跳到主要内容

7.切片

1.切片基础

1.切片的定义

在 Go 语言中,切片(slice) 是一种拥有相同类型元素的可变长度的序列,类似于动态数组。

  • 动态长度:切片的长度是可变的,适合需要频繁改变长度的数据结构。
  • 引用类型:切片是对底层数组的引用,因此修改切片内容会影响到其引用的底层数组。
  • 组成部分:切片包含一个指向底层数组的指针、长度(len)、以及容量(cap)。
  • 自动扩容:当切片追加元素并超出容量时,会自动生成一个新的底层数组并复制数据到新的数组中。

定义切片的基本语法

var name []T
// name:表示切片的变量名。
// T:表示切片中元素的类型。
  • var nums []int:声明了一个存储整数类型的切片,变量名为 nums
  • var names []string:声明了一个存储字符串类型的切片,变量名为 names

在 Go 语言中,字符串切片(slice of strings) 是一种引用类型,这意味着切片本身并不存储数据,而是引用底层数组中的数据。这种引用特性使得切片在赋值、传递函数参数或使用 = 赋值符号时,都只复制引用,不会复制数据本身。

package main

import "fmt"

func main() {
// 切片是引用类型,不支持直接比较,只能和 nil 比较
var a []string // 声明一个字符串切片
fmt.Println(a) // 输出:[]
fmt.Println(a == nil) // 输出:true

var b []int // 声明一个整数切片并初始化
fmt.Println(b) // 输出:[]
fmt.Println(b == nil) // 输出:false

var c = []bool{false, true} // 声明一个布尔切片并初始化
fmt.Println(c) // 输出:[false true]
fmt.Println(c == nil) // 输出:false
}

区别 nil 切片 和 空切片

  • var a []string:这是一个nil 切片,因为它只是声明了但没有进行初始化,也没有为它分配底层数组。a 直接被赋值为 nil,所以 a == nil 结果为 true
  • var b []int:虽然看起来和 a 的声明方式相同,但整数类型的切片 b 在这种情况下默认被初始化为空切片(长度和容量都是 0),而且它有一个指向的空的底层数组,所以 b == nil 结果为 false
  • c := []bool{false, true}:显然,c 已经通过字面量初始化,因此也不是 nilc == nil 结果为 false

切片之间不能直接用 == 操作符比较两个切片是否相等,唯一能进行的比较是与 nil 的比较。

一个 nil 切片的长度和容量都是 0,但长度和容量为 0 的切片不一定是 nil

package main

import "fmt"

func main() {
var slice = []int{1, 2, 3}
slice2 := slice
slice[0] = 3
fmt.Println(slice, slice2)
}

切片 slice 被初始化为 [1, 2, 3]

slice2 := slice 使得 slice2slice 指向同一个底层数组。

修改 slice[0] 的值会同时反映在 slice2 上,因为它们共享同一个底层数组。因此,输出结果为 [3, 2, 3] [3, 2, 3]

2. 切片的本质

切片的本质就是对底层数组的封装,它包含了三个信息; 底层数组的指针、切片的长度(len)和切片的容量(cap)

举个例子,现在有一个数组 a := [8] int {0,1,2,3,4,5,6,7}, 切片 s1 := a [5],相应示意图如下。

切片 s2 := a[3:6] ,相应示意图如下

[3:6]:切片的下界和上界,创建的新切片会包含从下标 3 到下标 5 的元素,但不包含下标 6 处的元素。

  • **下标 3:是开始位置,包含该位置的元素。
  • **下标 6:是结束位置,不包含该位置的元素。

** 左闭右开区间规则**

一致性

半开区间 [start:end] 设计能够使操作更加一致。例如,当 start == end 时,结果总是一个空切片,不管 startend 是什么值,这让代码更容易理解和预测。

例如:

s := a[3:3] // 结果是一个空切片,因为 3 == 3

这种情况在逻辑上很容易理解。如果是闭区间设计,这种情况下则可能引起混乱。

简化长度计算

使用半开区间 [start:end],切片的长度始终等于 end - start,这让计算长度变得更加直观和容易。

例如:

a := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
s := a[3:6] // 切片 s 包含 a[3], a[4], a[5]

在这种情况下,s 的长度为 6 - 3 = 3,非常直观。如果切片包含 end 位置的元素,长度的计算会变得稍微复杂。

简化拼接逻辑

当你需要将相邻的切片连接在一起时,半开区间的设计使得下标能够平滑地衔接。 例如:

a := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
s1 := a[0:3] // 包含元素 a[0], a[1], a[2]
s2 := a[3:6] // 包含元素 a[3], a[4], a[5]

如果你想把 s1s2 的元素连在一起,可以使用 a[0:6],因为 s1 的结束下标 3 恰好是 s2 的开始下标,不会产生重叠或遗漏的情况。

防止越界访问

如果允许包含 end 位置的元素,那么切片操作会需要额外的边界检查,防止出现访问数组越界的错误。而半开区间的规则确保 end 从不超出原始切片或数组的长度。

例如,如果你有一个长度为 10 的数组 a

a := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
s := a[8:10] // 安全,不越界,s 包含 a[8], a[9]

如果是闭区间设计,那么在这种情况下访问 a[10] 将会引发越界错误。

3. 切片的长度和容量

  • 切片拥有自己的长度和容量,可以通过内置的 len() 函数来获取长度,通过 cap() 函数获取切片的容量。
  • 切片的长度是其包含的元素个数。
  • 切片的容量是从第一个元素开始,到底层数组末尾的元素个数。
package main

import "fmt"

func main() {
a := [8]int{0, 1, 2, 3, 4, 5, 6, 7}
b := a[3:6]
fmt.Printf("值=%d, 长度=%d, 容量=%d\n", b, len(b), cap(b)) // 值=[3 4 5], 长度=3, 容量=5

c := b[:cap(b)]
fmt.Printf("值=%d, 长度=%d, 容量=%d", c, len(c), cap(c)) // 值=[3 4 5 6 7], 长度=5, 容量=5
}

这里的 cap(b) 表示切片 b 的容量大小。在你的例子中,b 是从数组 a[3:6] 切片得来的,其容量是 5(因为从 a[3]a[7] 有 5 个元素)。

2. 切片循环

切片的循环遍历和数组的循环遍历是一样的

2.1 基本遍历

package main

import "fmt"

func main() {
var a = []string{"北京", "上海", "深圳"} /*定义切片*/
for i := 0; i < len(a); i++ {
fmt.Println(a[i])
}
}

/*
北京
上海
深圳
*/

2.2 k,v 遍历

package main

import "fmt"

func main() {
var a = []string{"北京", "上海", "深圳"}
for index, value := range a {
fmt.Println(index, value)
}
}

/*
0 北京
1 上海
2 深圳
*/

使用 range 遍历for index, value := range a 使用 range 来遍历切片 a

  • index 表示当前元素的索引。
  • value 表示当前索引对应的值(切片的元素)。

3.append 函数

  • Go 语言的内建函数 append() 可以为切片动态添加元素,每个切片会指向一个底层数组。
  • 当切片的容量够用时,直接向底层数组中添加新元素。
  • 当底层数组的容量不足以容纳新增元素时,切片会自动根据一定策略进行“扩容”,此时切片指向的底层数组可能会更换。
  • “扩容” 操作经常在 append() 函数被调用时发生,因此通常需要用原变量接收 append() 函数的返回值。

append() 可以动态地向切片添加元素,切片在扩容时可能会指向一个新的底层数组,因此建议通过 a = append(a, ...) 的形式来确保切片引用的是扩容后的新数组。

3.1 append 添加

package main

import "fmt"

func main() {
// append() 添加元素和切片扩容
var numSlice []int
for i := 0; i < 10; i++ {
numSlice = append(numSlice, i)
fmt.Printf("%v len:%d cap:%d ptr:%p\n", numSlice, len(numSlice), cap(numSlice), numSlice)
}
}

代码解释:

  1. 定义切片var numSlice []int 定义了一个空的整型切片 numSlice,其初始长度和容量都是 0。
  2. 循环添加元素for i := 0; i < 10; i++ 循环 10 次,每次使用 append() 函数将当前的索引 i 添加到切片中。
  3. 打印切片状态:在每次添加元素后,通过 fmt.Printf 打印出当前的切片值、长度、容量以及切片的指针地址(内存地址)。
    • len(numSlice) 获取切片的长度。

    • cap(numSlice) 获取切片的容量。

    • numSlice 的指针地址用 %p 格式化输出。

    • %pfmt.Printf 函数的格式化动词,用于打印变量的指针值(内存地址)。

    • %d:用于打印整数(十进制)。

    • %s:用于打印字符串。

    • %f:用于打印浮点数。

    • %p:用于打印变量的内存地址(指针)。

    • %T:打印变量的类型。

    • %v 用于打印切片 numSlice 的当前内容(即切片中包含的元素)

输出结果:

[0] len:1 cap:1 ptr:0xc0000140e0
[0 1] len:2 cap:2 ptr:0xc000014120
[0 1 2] len:3 cap:4 ptr:0xc00001a0c0
[0 1 2 3] len:4 cap:4 ptr:0xc00001a0c0
[0 1 2 3 4] len:5 cap:8 ptr:0xc00001a120
[0 1 2 3 4 5] len:6 cap:8 ptr:0xc00001a120
[0 1 2 3 4 5 6] len:7 cap:8 ptr:0xc00001a120
[0 1 2 3 4 5 6 7] len:8 cap:8 ptr:0xc00001a120
[0 1 2 3 4 5 6 7 8] len:9 cap:16 ptr:0xc00001a180
[0 1 2 3 4 5 6 7 8 9] len:10 cap:16 ptr:0xc00001a180

3.2 append 添加多个

package main

import "fmt"

func main() {
var citySlice []string
citySlice = append(citySlice, "北京") // 添加一个元素
citySlice = append(citySlice, "上海", "广州", "深圳") // 添加多个元素
a := []string{"成都", "重庆"}
citySlice = append(citySlice, a...) // 添加切片
fmt.Println(citySlice) // [北京 上海 广州 深圳 成都 重庆]
}

代码解释:

  1. 声明切片var citySlice []string 定义了一个空的字符串切片 citySlice
  2. 添加单个元素citySlice = append(citySlice, "北京") 使用 append() 函数向切片中添加单个元素 "北京"
  3. 添加多个元素citySlice = append(citySlice, "上海", "广州", "深圳") 使用 append() 函数一次性向切片中添加多个元素。
  4. 添加另一个切片的所有元素a := []string{"成都", "重庆"} 定义了一个新的切片 a,然后使用 append(citySlice, a...) 将切片 a 的所有元素展开并添加到 citySlice 中。这里的 a... 是一个语法糖,用于将切片 a 拆开为独立的元素。
  5. 输出切片内容fmt.Println(citySlice) 最后输出整个切片,结果是 ["北京", "上海", "广州", "深圳", "成都", "重庆"]

当你要将一个切片 a 的所有元素添加到另一个切片时,使用 a... 来展开切片,这样才能正确地传递多个元素而不是传递一个嵌套的切片。

3.3 删除切片中的元素

append() 本质上是将两个切片连接起来形成一个新的切片。在删除操作中,你可以通过将不需要的元素“跳过”,再用 append() 函数将剩余的部分组合起来,最终生成一个没有被跳过元素的切片,从而实现删除的效果。

package main

import "fmt"

func main() {
a := []int{30, 31, 32, 33, 34, 35, 36, 37}
a = append(a[:2], a[3:]...) // 要删除索引为 2 的元素
fmt.Println(a) // [30 31 33 34 35 36 37]
}

代码解释:

  1. 初始切片a := []int{30, 31, 32, 33, 34, 35, 36, 37} 定义了一个包含 8 个整数的切片。
  2. 删除元素a = append(a[:2], a[3:]...) 的意思是删除索引为 2 的元素(即值为 32 的元素)。
    • a[:2] 表示切片从开头到索引 2(不包括 2)之前的所有元素,即 [30, 31]
    • a[3:] 表示从索引 3 开始到切片末尾的所有元素,即 [33, 34, 35, 36, 37]
    • append(a[:2], a[3:]...) 将这两部分合并,跳过索引为 2 的元素,从而完成删除操作。
  3. 输出结果fmt.Println(a) 打印出删除后的切片,结果是 [30, 31, 33, 34, 35, 36, 37]

输出结果:

[30 31 33 34 35 36 37]

3.4 切片合并

package main

import "fmt"

func main() {
arr1 := []int{2, 7, 1}
arr2 := []int{5, 9, 3}
fmt.Println(arr2, arr1)

arr1 = append(arr1, arr2...) // 合并 arr2 到 arr1
fmt.Println(arr1) // 输出: [2 7 1 5 9 3]
}

代码解释:

  1. 定义两个切片
    • arr1 := []int{2, 7, 1}:定义切片 arr1,包含元素 [2, 7, 1]
    • arr2 := []int{5, 9, 3}:定义切片 arr2,包含元素 [5, 9, 3]
  2. 使用 append() 合并切片
    • arr1 = append(arr1, arr2...):将 arr2 中的所有元素合并到 arr1。这里使用了 arr2... 来将 arr2 的元素展开,逐个添加到 arr1 中,而不是将 arr2 作为一个整体追加。
  3. 输出结果
    • 第一次打印 fmt.Println(arr2, arr1):输出两个切片的初始值,结果为 [5 9 3] [2 7 1]
    • 第二次打印 fmt.Println(arr1):输出合并后的 arr1,结果为 [2 7 1 5 9 3]

结果:

[5 9 3] [2 7 1]
[2 7 1 5 9 3]