今天我们继续讨论 切片(slice)。在本节中,我们将掌握:
-
如何复制切片
-
如何向切片追加元素
-
如何使用
range关键字遍历切片或数组
使用 range 遍历数组和切片
首先,假设我们有一个切片:
go
var s = []int{0, 10, -3, 5, 99}
代码说明:
声明了一个 int 类型的切片 s,包含 5 个元素。
使用普通 for 循环遍历切片
go
for index := 0; index < len(s); index++ {
// 处理切片元素
}
代码说明:
-
使用索引
index从0遍历到len(s)-1 -
通过
s[index]访问每个元素
使用 for range 循环
go
for index := range s {
// 处理切片元素
}
代码说明:
-
range s会自动遍历切片的索引 -
逻辑上与上一个
for循环等价 -
代码更简洁、可读性更好
同时获取索引和值
go
for index, element := range s {
// 处理切片元素
}
代码说明:
-
index:当前元素的索引 -
element:当前元素的 副本(copy)
只需要元素值,不需要索引
go
for _, element := range s {
// 处理切片元素
}
代码说明:
-
使用
_忽略索引 -
如果声明了但未使用索引变量,Go 编译器会报错
range 中的元素是副本(只读)
go
for _, element := range s {
element = newValue // 这个赋值不会生效
} // 原切片中的元素不会改变
代码说明:
-
element是切片元素的拷贝 -
修改它不会影响原切片
正确修改切片元素的方式
go
for index := range s {
s[index] = newValue
}
代码说明:
-
通过索引直接访问并修改切片中的元素
-
这是修改切片内容的正确方法
copy 函数
Go 提供了一个内置函数 copy 用来复制切片:
go
copy(destination, source)
-
第一个参数:目标切片
-
第二个参数:源切片
-
返回值:成功复制的元素个数
除了一个特殊情况(字符串),
copy只能用于切片字符串的情况会在后续章节介绍
copy 示例
go
var s = []int{12, 23, 34}
var sn = make([]int, len(s))
var n = copy(sn, s) // n 为复制的元素个数
sn[0] = 0
sn[1] = 11
fmt.Println(n) // 3
fmt.Println(s) // [12 23 34]
fmt.Println(sn) // [0 11 34]
代码说明:
-
sn是s的真正副本 -
修改
sn不会影响s -
n等于len(s),即 3
目标切片长度为 0 的情况
go
var s = []int{12, 23, 34}
var sn []int
var n = copy(sn, s)
fmt.Println(n) // 0
fmt.Println(sn) // []
代码说明:
-
sn的长度为 0,没有可用空间 -
copy不会复制任何元素
不关心返回值时
go
copy(sn, s)
代码说明:
- 如果复制的数量不重要,可以直接忽略返回值
append 函数
切片最重要的特性是 长度可变 。
Go 使用内置函数 append 向切片末尾追加元素。
向切片追加元素
go
var s = []int{12, 23, 34}
s = append(s, 45)
fmt.Println(s) // [12 23 34 45]
s = append(s, 56, 67)
fmt.Println(s) // [12 23 34 45 56 67]
代码说明:
-
append可以一次添加一个或多个元素 -
它会返回一个新的切片,必须接收返回值
追加一个切片到另一个切片
go
var s1 = []int{12, 23, 34}
var s2 = []int{45, 56, 67}
var s = append(s1, s2...)
代码说明:
-
...表示将s2拆分为多个参数 -
可以把一个切片整体追加到另一个切片中
等价写法(仅用于理解):
go
var s = append(s1, s2[0], s2[1], s2[2])
说明:
- 实际中切片长度不固定,因此必须使用
...
向 nil 切片追加元素
go
var s []int
s = append(s, 10)
代码说明:
-
append可以自动初始化一个 nil 切片 -
最终
s为[10]
切片的分配与重新分配(allocation & reallocation)
当切片容量不足时,append 会:
-
创建一个更大的底层数组
-
将原数据复制过去
为了减少这种开销,提前分配足够的容量是一个好习惯。
预先知道容量的情况
go
var a = []int{1, 2, 4, 3, 6}
var b = []int{-1, 9, -90}
var s = make([]int, 0, len(a)+len(b))
s = append(s, a...)
s = append(s, b...)
代码说明:
- 提前设置好容量,避免多次重新分配
根据经验预估容量
go
var countries = make([]string, 0, 2000)
代码说明:
-
长度为 0,容量为 2000
-
适合大量
append操作
只指定长度的情况
go
var countries = make([]string, 2000)
countries = append(countries, "Indonesia")
fmt.Println("cap:", cap(countries)) // cap: 3072
代码说明:
-
初始
len = cap = 2000 -
一旦
append,容量会自动扩展(重新分配)
直接通过索引赋值不会扩容
go
var countries = make([]string, 2000)
for i := range countries {
countries[i] = "Indonesia"
}
fmt.Println("cap:", cap(countries)) // cap: 2000
代码说明:
-
没有使用
append -
容量保持不变
总结
-
使用
for range遍历数组和切片 -
range中的元素是副本,不能直接修改 -
copy函数用于复制切片 -
append函数用于扩展切片 -
切片的容量、重新分配以及性能优化技巧