Golang 中的数组与 slice
数组
在Go语言中,数组是一种固定长度且元素类型一致的序列。数组的长度在定义时就已确定,并且在数组的生命周期中无法改变。
数组的特性:
- 固定长度: 数组的长度在定义时就已确定,无法在运行时改变。
- 零值初始化: 未显式初始化的数组元素会被自动赋予类型的零值(例如,整数类型的零值为0,字符串类型的零值为空字符串,指针类型的零值为nil)。
- 值语义: 当数组被赋值给另一个数组或作为参数传递给函数时,实际是对整个数组进行拷贝,而不是引用传递。
- 多维数组: Go支持多维数组。
创建数组的方式:
- 通过字面量创建:
var arr1 [5]int // 声明一个长度为5的整数数组,默认值为0
arr2 := [3]string{"a", "b", "c"} // 声明并初始化一个长度为3的字符串数组
- 通过指定长度和初始值创建:
arr := [...]int{1, 2, 3, 4, 5} // 编译器会根据初始值的个数推断数组的长度
- 多维数组:
var matrix [3][3]int // 声明一个3x3的二维整数数组
Slice
slice
(切片)是一种动态数组,它提供了一种方便且高效的方式来操作序列化的数据集合。Slice
是对数组的一个抽象,它通过指向数组的指针、长度和容量来描述数组的一段连续片段。
切片的底层结构
切片在Go语言的底层是由三个字段组成的结构体:
- 指针(Pointer):指向底层数组的第一个元素。
- 长度(Length):切片中的元素个数。
- 容量(Capacity):从切片的第一个元素开始到底层数组的最后一个元素的个数。
type slice struct {
array unsafe.Pointer
len int
cap int
}
Slice的特性:
- 动态大小: Slice可以动态增长或缩减,而无需复制整个数组。
- 引用语义: 当传递slice给函数或方法时,实际上传递的是指向底层数组的引用,因此对slice的修改会影响底层数组的对应部分。
- 零值: slice的零值为nil,表示未指向任何数组。
- 长度和容量:
- 长度(length): 表示slice当前包含的元素个数,可以通过内置函数
len()
获取。 - 容量(capacity): 表示slice开始位置到底层数组末尾的元素个数,可以通过内置函数
cap()
获取。
- 长度(length): 表示slice当前包含的元素个数,可以通过内置函数
创建slice的方式:
- 使用make函数:
make([]T, length, capacity)
,其中T
是slice中元素的类型,length
是初始长度,capacity
是预分配的容量。
slice := make([]int, 5, 10) // 创建一个初始长度为5,容量为10的int类型slice
- 通过字面量: 直接通过数组或其他slice来创建。
arr := [5]int{1, 2, 3, 4, 5}
slice := arr[1:4] // 创建一个从数组arr索引1到索引3的slice,包含元素{2, 3, 4}
操作slice:
- 访问元素: 可以像数组一样使用索引访问slice中的元素。
- 追加元素: 使用内置函数
append()
向slice中追加元素。如果slice的容量不足,append()
函数会自动扩展容量。
slice := []int{1, 2, 3}
slice = append(slice, 4) // 追加元素4到slice中
- 复制slice: 使用内置函数
copy()
复制一个slice到另一个slice。
slice1 := []int{1, 2, 3}
slice2 := make([]int, len(slice1))
copy(slice2, slice1) // 复制slice1到slice2
数组与切片的区别
特性 | 数组 | 切片 |
---|---|---|
长度 | 固定,定义时确定,不能改变 | 动态,可增长或缩减 |
零值 | 数组类型的零值是包含零值的数组 | 切片类型的零值是nil |
内存分配 | 内存一次性分配 | 内存按需分配 |
值/引用 | 值类型,赋值或传参时会拷贝整个数组 | 引用类型,赋值或传参时只拷贝引用 |
创建方式 | var arr [5]int | make([]int, len, cap) 或 []int{} |
使用场景 | 用于需要固定大小的集合 | 用于需要动态大小的集合 |
示例代码:
package main
import "fmt"
func main() {
// 数组示例
var arr [3]int = [3]int{1, 2, 3}
fmt.Println("数组:", arr)
// 修改数组元素
arr[1] = 20
fmt.Println("修改后的数组:", arr)
// 切片示例
slice := []int{1, 2, 3}
fmt.Println("切片:", slice)
// 向切片追加元素
slice = append(slice, 4)
fmt.Println("追加元素后的切片:", slice)
// 切片切割
newSlice := slice[1:3]
fmt.Println("切片切割:", newSlice)
}
Slice 的扩容
Go语言中的切片具有动态扩容的特性,当向切片追加元素且容量不足时,切片会自动扩容。切片的扩容机制是通过内置函数append
实现的。理解切片的扩容原理有助于编写高效的Go代码。
扩容的过程
当使用append
函数向切片追加元素时,如果切片的容量不足以容纳新元素,Go会自动执行以下步骤来扩容:
- 分配新的底层数组:根据当前切片的容量和即将追加的元素数量,计算新的容量,并分配一个新的底层数组。
- 复制数据:将旧的底层数组中的数据复制到新的底层数组中。
- 更新指针和容量:将切片的指针指向新的底层数组,并更新切片的容量。
容量增长策略
Go语言采用了一种几何增长策略来扩容切片,以确保扩容操作的效率。具体策略如下:
- 当切片的当前容量小于1024时,新的容量将是旧容量的两倍。
- 当切片的当前容量大于或等于1024时,新的容量将增加当前容量的1/4。
以下是一个示例,展示了切片在追加元素时的扩容过程:
package main
import "fmt"
func main() {
slice := make([]int, 0, 2)
fmt.Printf("初始容量: %d\n", cap(slice))
// 追加元素,触发扩容
for i := 0; i < 10; i++ {
slice = append(slice, i)
fmt.Printf("追加元素 %d 后的长度: %d, 容量: %d\n", i, len(slice), cap(slice))
}
}
示例输出
初始容量: 2
追加元素 0 后的长度: 1, 容量: 2
追加元素 1 后的长度: 2, 容量: 2
追加元素 2 后的长度: 3, 容量: 4
追加元素 3 后的长度: 4, 容量: 4
追加元素 4 后的长度: 5, 容量: 8
追加元素 5 后的长度: 6, 容量: 8
追加元素 6 后的长度: 7, 容量: 8
追加元素 7 后的长度: 8, 容量: 8
追加元素 8 后的长度: 9, 容量: 16
追加元素 9 后的长度: 10, 容量: 16
注意事项
- 性能影响:频繁的扩容操作会带来性能开销,因为每次扩容都涉及到内存分配和数据复制。因此,如果可以预估切片的最终大小,最好在创建切片时直接分配足够的容量。
- 内存浪费:由于切片的容量通常是按倍数增加的,有时会分配多余的内存空间,导致一定的内存浪费。
内存共享问题
切片中的内存共享机制意味着多个切片可以引用同一个底层数组的不同部分,从而实现高效的数据操作和传递。
内存共享特性
- 共享底层数组:多个切片可以共享同一个底层数组的不同部分。当一个切片的元素发生改变时,其他引用同一个底层数组的切片也会受到影响。
- 引用传递:切片是引用类型,当切片被传递给函数或被赋值给另一个切片时,实际上传递的是底层数组的引用,而不是整个数组的拷贝。
示例代码
package main
import "fmt"
func main() {
// 创建一个底层数组
arr := [5]int{1, 2, 3, 4, 5}
// 创建两个切片,分别引用底层数组的不同部分
slice1 := arr[0:3]
slice2 := arr[2:5]
fmt.Println("初始状态:")
fmt.Println("arr:", arr)
fmt.Println("slice1:", slice1)
fmt.Println("slice2:", slice2)
// 修改slice1的元素
slice1[2] = 100
fmt.Println("\n修改slice1[2]后的状态:")
fmt.Println("arr:", arr)
fmt.Println("slice1:", slice1)
fmt.Println("slice2:", slice2)
}
示例输出
初始状态:
arr: [1 2 3 4 5]
slice1: [1 2 3]
slice2: [3 4 5]
修改slice1[2]后的状态:
arr: [1 2 100 4 5]
slice1: [1 2 100]
slice2: [100 4 5]
在这个示例中,slice1
和slice2
共享同一个底层数组arr
。修改slice1
中的元素会影响到slice2
,因为它们都引用了相同的底层数组。
注意事项
- 数据一致性:由于切片共享底层数组,修改一个切片中的数据会影响到其他引用同一底层数组的切片。这在某些情况下可能会引发数据一致性问题,需要特别注意。
- 避免误用:在进行切片操作时,特别是在传递切片给函数或在不同部分间共享切片时,要小心处理,避免意外修改共享数据。
- 容量限制:切片的容量是从切片的起始位置到底层数组末尾的元素个数。如果切片操作超过其容量范围,可能会导致运行时错误或意外的行为。
深拷贝与浅拷贝
- 浅拷贝:切片赋值或传递时是浅拷贝,只复制切片结构体中的指针、长度和容量,底层数组不复制。
- 深拷贝:若需要独立的副本,可以通过内置函数
copy
进行深拷贝,创建一个新的底层数组并复制数据。
package main
import "fmt"
func main() {
// 创建一个底层数组
arr := [5]int{1, 2, 3, 4, 5}
slice1 := arr[:]
// 深拷贝
slice2 := make([]int, len(slice1))
copy(slice2, slice1)
fmt.Println("初始状态:")
fmt.Println("slice1:", slice1)
fmt.Println("slice2:", slice2)
// 修改slice1的元素
slice1[2] = 100
fmt.Println("\n修改slice1[2]后的状态:")
fmt.Println("slice1:", slice1)
fmt.Println("slice2:", slice2)
}
示例输出
初始状态:
slice1: [1 2 3 4 5]
slice2: [1 2 3 4 5]
修改slice1[2]后的状态:
slice1: [1 2 100 4 5]
slice2: [1 2 3 4 5]
在这个示例中,slice2
是slice1
的深拷贝,修改slice1
的元素不会影响slice2
。
子切片与原切片
子切片与原始切片之间的内存共享机制是一个重要特性。子切片(subslices)是从原始切片(parent slice)中截取出来的一部分,它们共享相同的底层数组。
子切片的创建
子切片是通过切片操作符 slice[low:high]
从原始切片中创建的,其中 low
和 high
是索引,表示子切片的起始位置(包含)和结束位置(不包含)。
内存共享机制
- 共享底层数组:子切片和原始切片共享同一个底层数组。这意味着对子切片的修改可能会影响原始切片,反之亦然,因为它们都指向相同的底层数据。
- 长度和容量:子切片的长度是
high - low
,容量是从low
开始到底层数组末尾的元素个数。
示例代码
package main
import "fmt"
func main() {
// 创建一个原始切片
original := []int{1, 2, 3, 4, 5}
// 创建子切片
subSlice := original[1:4]
fmt.Println("初始状态:")
fmt.Println("original:", original)
fmt.Println("subSlice:", subSlice)
// 修改子切片中的元素
subSlice[1] = 100
fmt.Println("\n修改subSlice[1]后的状态:")
fmt.Println("original:", original)
fmt.Println("subSlice:", subSlice)
}
示例输出
初始状态:
original: [1 2 3 4 5]
subSlice: [2 3 4]
修改subSlice[1]后的状态:
original: [1 2 100 4 5]
subSlice: [2 100 4]
在这个示例中,subSlice
是从 original
切片中创建的。修改 subSlice
的元素会影响 original
,因为它们共享同一个底层数组。
注意事项
- 数据一致性:由于内存共享,修改子切片中的数据会影响原始切片,这在某些情况下可能会引发数据一致性问题。需要特别小心在并发环境下的切片操作,以避免竞态条件。
- 容量限制:子切片的容量是从
low
开始到底层数组末尾的元素个数。如果子切片的操作超过其容量范围,可能会导致运行时错误或未定义行为。
扩展子切片的影响
当使用 append
函数向子切片添加元素时,如果添加的元素使得子切片的容量不足,Go会分配一个新的底层数组,并将现有元素复制到新的数组中。这时,原始切片和子切片将不再共享同一个底层数组。
package main
import "fmt"
func main() {
// 创建一个原始切片
original := []int{1, 2, 3, 4, 5}
// 创建子切片
subSlice := original[1:4]
// 向子切片追加元素,触发扩容
subSlice = append(subSlice, 6, 7, 8)
fmt.Println("追加元素后的状态:")
fmt.Println("original:", original)
fmt.Println("subSlice:", subSlice)
}
示例输出
追加元素后的状态:
original: [1 2 3 4 5]
subSlice: [2 3 4 6 7 8]
在这个示例中,向 subSlice
追加元素导致了切片的扩容,新的元素被添加到一个新的底层数组中,subSlice
和 original
切片之间的共享关系被打破。
深拷贝子切片
如果需要创建一个独立的子切片,而不希望它与原始切片共享内存,可以使用 copy
函数来实现深拷贝。
package main
import "fmt"
func main() {
// 创建一个原始切片
original := []int{1, 2, 3, 4, 5}
// 创建子切片
subSlice := original[1:4]
// 创建一个独立的副本
independentSlice := make([]int, len(subSlice))
copy(independentSlice, subSlice)
fmt.Println("初始状态:")
fmt.Println("original:", original)
fmt.Println("subSlice:", subSlice)
fmt.Println("independentSlice:", independentSlice)
// 修改子切片中的元素
subSlice[1] = 100
fmt.Println("\n修改subSlice[1]后的状态:")
fmt.Println("original:", original)
fmt.Println("subSlice:", subSlice)
fmt.Println("independentSlice:", independentSlice)
}
示例输出
初始状态:
original: [1 2 3 4 5]
subSlice: [2 3 4]
independentSlice: [2 3 4]
修改subSlice[1]后的状态:
original: [1 2 100 4 5]
subSlice: [2 100 4]
independentSlice: [2 3 4]
在这个示例中,independentSlice
是 subSlice
的深拷贝,修改 subSlice
的元素不会影响 independentSlice
。
总结
本文深度对比了golang中的数组和切片,重点介绍了切片的常见操作与常见的误区。要非常注意切片的扩容策略,内存共享问题。这些都是面试中的要点。