09 复合类型类型 - 数组与切片的使用和注意事项 -3
切片零值
- 切片的零值是
nil,这意味着一个未初始化的切片会等于nil。 - 零值切片可以通过
append进行扩展,而不会发生运行时错误。 - 零值切片没有底层数组,它的
len(长度)和cap(容量)都为 0,但 Go 会在需要时动态分配数组来存储数据。 - 和数组不同,切片的零值可以安全地使用,即使它是
nil,仍可以对它进行操作(如append),而不会发生运行时错误。可以理解为切片的零值相当于一个未指向任何底层数组的空切片。
go
var slice []int // 声明一个零值切片
fmt.Println(slice == nil) // true,表示切片未被初始化
fmt.Printf("len = %d, cap = %d\n", len(slice), cap(slice)) // len = 0, cap = 0
slice = append(slice, 1) // 可以直接 append
fmt.Println(slice) // 输出:[1]零值切片的特点
等于
nil:未初始化的切片是nil。govar slice []int fmt.Println(slice == nil) // true长度和容量为 0:零值切片的长度和容量都是 0。
gofmt.Println(len(slice)) // 0 fmt.Println(cap(slice)) // 0可以使用
append:即使是零值切片,append也可以正常使用,Go 会在底层分配新的数组存储追加的元素。goslice = append(slice, 1, 2, 3) fmt.Println(slice) // 输出:[1 2 3]无需手动初始化:如果你不确定切片是否被初始化,可以直接使用
append而无需显式地检查它是否是nil。
与 make() 创建的切片对比
make() 创建的切片并不会等于 nil,即使长度为 0,因为 make() 会为切片分配一个底层数组。
go
var slice1 []int // 零值切片,等于 nil
slice2 := make([]int, 0) // 长度为 0,容量为 0 的切片
fmt.Println(slice1 == nil) // true,表示 slice1 是 nil
fmt.Println(slice2 == nil) // false,slice2 被初始化,不等于 nil
fmt.Println(len(slice1)) // 0
fmt.Println(len(slice2)) // 0何时使用零值切片
- 在不确定切片大小时,可以安全地声明一个零值切片,稍后通过
append动态添加数据。 - 在某些接口中,你可以返回一个零值切片(
nil)来表示不存在任何结果,而不是返回一个空的切片。
切片的遍历
- Go 提供了两种方式遍历切片:通过索引或使用
range关键字。 range更加简洁,也更具可读性。
使用 for 循环通过索引遍历
- 适合在遍历过程中需要使用索引进行一些操作,如通过索引修改元素。
go
slice := []int{1, 2, 3, 4, 5}
for i := 0; i < len(slice); i++ {
fmt.Printf("Index: %d, Value: %d\n", i, slice[i])
}
// 输出:
// Index: 0, Value: 1
// Index: 1, Value: 2
// Index: 2, Value: 3
// Index: 3, Value: 4
// Index: 4, Value: 5使用 range 关键字遍历
go
slice := []int{1, 2, 3, 4, 5}
// i 是索引,v 是值
for i, v := range slice {
fmt.Printf("Index: %d, Value: %d\n", i, v)
}
// 输出:
// Index: 0, Value: 1
// Index: 1, Value: 2
// Index: 2, Value: 3
// Index: 3, Value: 4
// Index: 4, Value: 5
// 只遍历值,忽略索引
for _, v := range slice {
fmt.Printf("Value: %d\n", v)
}
// 输出:
// Value: 1
// Value: 2
// Value: 3
// Value: 4
// Value: 5切片的复制
Go 提供了内置的 copy() 函数用于在切片之间进行元素复制。
使用 copy() 复制切片
go
src := []int{1, 2, 3}
dst := make([]int, len(src)) // 创建目标切片,长度等于 src
copy(dst, src) // 复制 src 到 dst
fmt.Println("dst:", dst) // 输出:[1, 2, 3]copy(dst, src)会将src中的元素复制到dst中。如果dst的长度小于src,只会复制部分元素。copy()返回复制的元素数量。
部分复制
copy 会根据目标切片的长度进行复制,多余的元素会被忽略。
go
src := []int{1, 2, 3, 4, 5}
dst := make([]int, 2) // 创建长度为 2 的目标切片
n := copy(dst, src) // 只复制 src 的前两个元素
fmt.Println("dst:", dst, "Copied:", n) // 输出:[1, 2] Copied: 2切片的扩容
- 通过
append()可以动态添加元素,当切片容量不足时,Go 会自动扩展底层数组的容量。 - 如果切片的容量不足,Go 会为你分配一个新的、更大的底层数组。
- 当容量不足时,Go 会自动将容量增加到当前容量的 2 倍(或更大,具体取决于已有数据的大小),以减少频繁扩容带来的性能损耗。
使用 append() 动态扩容
go
slice := []int{1, 2, 3} // len = 3, cap = 3
slice = append(slice, 4) // 添加一个元素,len = 4, cap = 6
slice = append(slice, 5, 6, 7) // 一次添加多个元素,len = 7, cap = 12
fmt.Printf("slice = %v, len = %d, cap = %d\n", slice, len(slice), cap(slice))
// 输出:slice = [1, 2, 3, 4, 5, 6, 7], len = 7, cap = 12控制扩容逻辑
在一些场景下,提前分配足够的容量可以避免不必要的扩容和内存重新分配。
go
slice := make([]int, 0, 10) // len = 0, cap = 10
slice = append(slice, 1, 2, 3) // 即使添加了 3 个元素,切片仍然有充足的容量。len = 3, cap = 10
fmt.Printf("slice = %v, len = %d, cap = %d\n", slice, len(slice), cap(slice))
// 输出:slice = [1, 2, 3], len = 3, cap = 10切片的扩容规则
- 如果新的容量大于 2 倍原容量,则新容量就是新申请的容量。
- 否则,如果原切片长度 < 1024,则新容量为原来的 2 倍;如果原切片长度 >= 1024,则每次增加原来容量的 1/4,直到新容量 > 新申请的容量。
go
// 示例:使用 append 追加元素
slice := []int{1, 2, 3} // len = 3, cap = 3
slice = append(slice, 4) // 追加 4,len = 4, cap = 6
slice = append(slice, 5, 6, 7) // 追加 5, 6, 7,len = 7, cap = 12
fmt.Printf("slice = %v, len = %d, cap = %d\n", slice, len(slice), cap(slice))
// 输出:slice = [1 2 3 4 5 6 7], len = 7, cap = 12扩容后切片解绑
go
// 使用字面量初始化 slice
slice := []int{1, 2, 3, 4, 5}
fmt.Printf("Initial slice: %v, len = %d, cap = %d\n", slice, len(slice), cap(slice))
// 通过截取生成 newSlice
newSlice := slice[1:4] // [2, 3, 4], len=3, cap=4
fmt.Printf("newSlice before modification: %v, len = %d, cap = %d\n", newSlice, len(newSlice), cap(newSlice))
// 修改 newSlice 的元素,观察 slice 是否改变
newSlice[0] = 100
fmt.Println("\nAfter modifying newSlice before append:")
fmt.Printf("slice: %v\n", slice)
fmt.Printf("newSlice: %v\n", newSlice)
// 对 newSlice 进行扩容(触发扩容)
newSlice = append(newSlice, 6, 7, 8) // 扩容,生成新数组
fmt.Println("\nAfter append (expansion):")
fmt.Printf("slice: %v\n", slice)
fmt.Printf("newSlice: %v, len = %d, cap = %d\n", newSlice, len(newSlice), cap(newSlice))
// 扩容后修改 newSlice 的元素,观察 slice 是否改变
newSlice[0] = 200
fmt.Println("\nAfter modifying newSlice after append:")
fmt.Printf("slice: %v\n", slice)
fmt.Printf("newSlice: %v\n", newSlice)扩容前修改:
newSlice := slice[1:4]生成的切片与slice共享同一个底层数组,newSlice初始为[2, 3, 4]。- 修改
newSlice[0] = 100后,newSlice中的第一个元素变成了100,因为它与slice共享同一个底层数组,所以slice中的对应元素也改变为100,即slice变为[1, 100, 3, 4, 5]。
扩容后:
- 当
newSlice使用append添加元素6, 7, 8时,触发了扩容。扩容会导致 Go 为newSlice分配一个新的底层数组,将原来的元素[100, 3, 4]复制到新数组中。因此,slice和newSlice不再共享同一个底层数组。 - 扩容后
slice保持不变,仍然是[1, 100, 3, 4, 5],而newSlice的内容变为[100, 3, 4, 6, 7, 8],长度变为 6,容量变为 8。
扩容后修改:
- 修改
newSlice[0] = 200时,由于newSlice和slice已经解绑,slice不再受到影响。因此,slice保持为[1, 100, 3, 4, 5],而newSlice变为[200, 3, 4, 6, 7, 8]。
bash
Initial slice: [1 2 3 4 5], len = 5, cap = 5
newSlice before modification: [2 3 4], len = 3, cap = 4
After modifying newSlice before append:
slice: [1 100 3 4 5]
newSlice: [100 3 4]
After append (expansion):
slice: [1 100 3 4 5]
newSlice: [100 3 4 6 7 8], len = 6, cap = 8
After modifying newSlice after append:
slice: [1 100 3 4 5]
newSlice: [200 3 4 6 7 8]- 扩容前:
slice和newSlice共享同一个底层数组,修改newSlice会影响slice。 - 扩容后:
newSlice和slice解绑,修改newSlice不再影响slice。
切片初始化建议
- 在生产环境中,如何初始化和管理切片的容量是非常关键的。
- 适当的初始化不仅可以提高程序的性能,还可以避免不必要的内存分配。
根据预估的容量进行初始化
- 如果可以预估切片中可能存储的数据量,最好使用
make([]T, len, cap)来预先分配合适的容量,避免扩容带来的性能开销。 - 这种方法适用于数据量较大的场景,比如批量处理、大数据集等。
go
slice := make([]int, 0, 100) // 创建容量为 100 的切片在批量插入时提前分配容量
- 对于需要一次性插入大量元素的场景,提前分配好容量有助于避免频繁扩容。
- 这样可以保证一次性完成容量的分配,避免多次
append()时扩容。
go
n := 1000 // 预估需要存储 1000 个元素
slice := make([]int, 0, n) // 提前分配好容量
for i := 0; i < n; i++ {
slice = append(slice, i)
}动态扩容时减少内存占用
- 在一些场景下,不清楚需要多少容量时,可以让 Go 自己处理扩容机制。
- 但需要注意,过度扩容可能会导致内存浪费。如果切片不再需要增长,可以通过切片截取缩小切片的容量。
go
slice = slice[:len(slice):len(slice)] // 将容量调整为和长度一致使用 copy() 进行切片优化
- 在一些对性能敏感的场景下,可以考虑复制重要的数据到新的切片中,避免不必要的共享底层数组。
go
slice := []int{1, 2, 3, 4, 5}
// 要删除前几个元素而不希望共享底层数组
newSlice := make([]int, len(slice[2:]))
copy(newSlice, slice[2:]) // 从索引 2 开始复制数据
fmt.Println(newSlice) // 输出:[3, 4, 5]
// newSlice 不会再共享 slice 的底层数组,避免不必要的内存使用