GO的数组与切片
2025/9/5...大约 8 分钟
1 数组(Array)基础
1.1 数组的定义
数组是一个固定长度的相同类型元素的集合。在 Go 中,数组的长度是其类型的一部分,一旦定义就不能改变。
// 定义一个长度为5的整型数组
var a [5]int
// 定义并初始化一个数组
var b [3]int = [3]int{1, 2, 3}
// 类型推导简化初始化
c := [3]int{1, 2, 3}
// 自动计算长度
c := [...]int{1, 2, 3} // 等价于 [3]int{1, 2, 3}
// 指定索引初始化
d := [5]int{0: 1, 2: 3, 4: 5} // [1 0 3 0 5]1.2 数组的访问和修改
使用索引来访问和修改数组元素,索引从0开始:
arr := [5]int{1, 2, 3, 4, 5}
// 访问元素
fmt.Println(arr[0]) // 输出: 1
fmt.Println(arr[4]) // 输出: 5
// 修改元素
arr[2] = 10
fmt.Println(arr) // 输出: [1 2 10 4 5]
// 遍历数组
sum := 0
for i := 0; i < len(arr); i++ {
sum += arr[i]
}
// 使用 range 遍历
for index, value := range arr {
fmt.Printf("索引: %d, 值: %d\n", index, value)
}
// 只关注值,忽略索引
for _, value := range arr {
fmt.Printf("值: %d\n", value)
}1.3 数组的内存布局
在 Go 中,数组是值类型。当你将一个数组赋值给另一个数组,或者将数组作为参数传递给函数时,会发生数组的完整复制。
arr1 := [3]int{1, 2, 3}
arr2 := arr1 // 复制整个数组
arr2[0] = 10
fmt.Println(arr1) // 输出: [1 2 3] - arr1 未被修改
fmt.Println(arr2) // 输出: [10 2 3] - arr2 被修改数组在内存中是连续存储的:
[1][2][3][4][5]
^ ^
索引0 索引41.4 多维数组
Go 支持多维数组:
// 二维数组
var matrix [3][3]int
// 初始化二维数组
identity := [3][3]int{
{1, 0, 0},
{0, 1, 0},
{0, 0, 1},
}
// 访问二维数组元素
fmt.Println(identity[1][1]) // 输出: 12 切片(Slice)基础
2.1 切片的定义
切片是对数组的引用,它提供了一种灵活、动态的方式来操作数组的一部分。切片是引用类型,它本身不存储数据,而是指向底层的数组。
// 定义一个整型切片
var s []int
// 使用 make 创建切片,格式为 make([]T, len, cap)
// len 是切片的长度,cap 是切片的容量(可选)
s1 := make([]int, 5) // 长度为5,容量为5
s2 := make([]int, 5, 10) // 长度为5,容量为10
// 从数组创建切片
a := [5]int{1, 2, 3, 4, 5}
s3 := a[1:3] // 从索引1开始,到索引3结束(不包含索引3),结果: [2 3]
s4 := a[:3] // 从索引0开始,到索引3结束,结果: [1 2 3]
s5 := a[2:] // 从索引2开始,到数组末尾,结果: [3 4 5]
s6 := a[:] // 整个数组,结果: [1 2 3 4 5]
// 直接初始化切片
s7 := []int{1, 2, 3, 4, 5}2.2 切片的核心结构
切片在内部由三个部分组成:
- 指向底层数组的指针
- 切片的长度(len):切片中元素的数量
- 切片的容量(cap):从切片的起始位置到底层数组末尾的元素数量
s := make([]int, 5, 10)
fmt.Printf("长度: %d, 容量: %d\n", len(s), cap(s)) // 输出: 长度: 5, 容量: 102.3 切片的操作
2.3.1 添加元素
使用 append 函数向切片添加元素:
s := []int{1, 2, 3}
s = append(s, 4) // [1 2 3 4]
s = append(s, 5, 6) // [1 2 3 4 5 6]
// 合并两个切片
s2 := []int{7, 8, 9}
s = append(s, s2...) // [1 2 3 4 5 6 7 8 9]2.3.2 切片的复制
使用 copy 函数复制切片:
src := []int{1, 2, 3, 4, 5}
dst := make([]int, 3)
n := copy(dst, src) // dst: [1 2 3], n: 3
// 如果目标切片容量小于源切片
dst2 := make([]int, 2)
n = copy(dst2, src) // dst2: [1 2], n: 2
// 如果目标切片长度为0但有容量
dst3 := make([]int, 0, 5)
n = copy(dst3, src) // dst3: [], n: 02.3.3 切片的遍历
与数组类似,可以使用 for 循环或 range 遍历切片:
s := []int{1, 2, 3, 4, 5}
// 使用索引遍历
for i := 0; i < len(s); i++ {
fmt.Printf("索引: %d, 值: %d\n", i, s[i])
}
// 使用 range 遍历
for index, value := range s {
fmt.Printf("索引: %d, 值: %d\n", index, value)
}3 切片的扩容机制
3.1 自动扩容
当使用 append 向切片添加元素导致长度超过容量时,Go 会自动创建一个更大的底层数组,并将原数组的数据复制到新数组中,然后返回指向新数组的切片。
s := make([]int, 0, 3) // 长度0,容量3
s = append(s, 1, 2, 3) // 长度3,容量3,底层数组: [1 2 3]
s = append(s, 4) // 触发扩容,创建新数组,长度4,容量可能为6,底层数组: [1 2 3 4]3.2 扩容策略
Go 的切片扩容策略大致如下:
- 当原切片容量小于 1024 时,新切片的容量通常是原容量的 2 倍
- 当原切片容量大于等于 1024 时,新切片的容量通常是原容量的 1.25 倍
- 在某些情况下,扩容还会考虑内存对齐因素
// 容量小于 1024 的情况
s1 := make([]int, 0, 10) // 容量10
for i := 0; i < 20; i++ {
s1 = append(s1, i)
fmt.Printf("i=%d, len=%d, cap=%d\n", i, len(s1), cap(s1))
// 当 i=9 时,容量会从10扩展到20
}
// 容量大于等于 1024 的情况
s2 := make([]int, 0, 1024) // 容量1024
for i := 0; i < 1200; i++ {
if i == 1024 {
fmt.Printf("扩容后: len=%d, cap=%d\n", len(s2), cap(s2))
// 容量会从1024扩展到约1344 (1024*1.25=1280,可能由于内存对齐扩展到1344)
}
s2 = append(s2, i)
}4 数组与切片的区别
| 特性 | 数组 | 切片 |
|---|---|---|
| 类型 | 值类型 | 引用类型 |
| 长度 | 固定,是类型的一部分 | 可变,可以动态扩容 |
| 内存布局 | 直接存储数据 | 包含指针、长度、容量 |
| 赋值/传递 | 复制整个数组 | 复制切片的引用结构 |
| 初始化 | 使用 [n]T{...} | 使用 make([]T, len, cap) 或切片表达式 |
| 零值 | 所有元素都是对应类型的零值 | nil |
5 注意事项
5.1 数组作为函数参数
由于数组是值类型,当作为函数参数传递时,会复制整个数组,这可能导致性能问题。如果需要修改原数组或避免复制,应该使用指针:
// 不推荐:复制整个数组
func modifyArray(arr [5]int) {
arr[0] = 100
// 这里修改的是副本,不影响原数组
}
// 推荐:传递数组指针
func modifyArrayByPointer(arr *[5]int) {
arr[0] = 100
// 这里修改的是原数组
}5.2 切片的共享底层数组
多个切片可能共享同一个底层数组,修改一个切片可能会影响其他切片:
a := [5]int{1, 2, 3, 4, 5}
s1 := a[1:4] // [2 3 4]
s2 := a[2:5] // [3 4 5]
s1[1] = 100 // 同时修改了s1和s2共享的元素
fmt.Println(s1) // [2 100 4]
fmt.Println(s2) // [100 4 5]
fmt.Println(a) // [1 2 100 4 5]5.3 切片的扩容与内存管理
切片扩容会创建新的底层数组并复制数据,这是一个开销较大的操作。在知道大致需要的切片大小时,可以预先分配足够的容量,以减少扩容操作:
// 不推荐:频繁扩容
func bad() {
s := []int{}
for i := 0; i < 10000; i++ {
s = append(s, i) // 会多次触发扩容
}
}
// 推荐:预先分配容量
func good() {
s := make([]int, 0, 10000) // 预先分配足够的容量
for i := 0; i < 10000; i++ {
s = append(s, i) // 不会触发扩容
}
}5.4 nil 切片与空切片
nil 切片和空切片都不包含任何元素,但它们有细微的区别:
var s1 []int // nil 切片,len=0, cap=0, ==nil
s2 := []int{} // 空切片,len=0, cap=0, !=nil
s3 := make([]int, 0) // 空切片,len=0, cap=0, !=nil
fmt.Println(s1 == nil) // true
fmt.Println(s2 == nil) // false
fmt.Println(s3 == nil) // false在大多数情况下,nil 切片和空切片的行为是相同的,都可以正常使用 append 等操作。
5.5 避免切片的内存泄漏
如果一个大切片中只有一小部分被使用,但该切片被长期引用,那么整个底层数组都不会被垃圾回收。解决方法是创建一个新的切片,只包含需要的部分:
func getData() []int {
largeData := make([]int, 10000) // 大数组
// ... 填充数据 ...
// 只需要其中一小部分
return largeData[9900:10000] // 返回子切片,但仍引用整个大数组
}
// 改进版本
func getDataFixed() []int {
largeData := make([]int, 10000)
// ... 填充数据 ...
// 创建一个新切片,只复制需要的部分
result := make([]int, 100)
copy(result, largeData[9900:10000])
return result
}6 实际应用示例
6.1 字符串与字节切片
字符串和字节切片之间可以相互转换:
// 字符串转字节切片
str := "hello"
b := []byte(str)
// 字节切片转字符串
b = []byte{'w', 'o', 'r', 'l', 'd'}
str = string(b)6.2 使用切片实现栈和队列
// 栈的实现
stack := make([]int, 0)
// 入栈
stack = append(stack, 1, 2, 3)
// 出栈
if len(stack) > 0 {
top := stack[len(stack)-1]
stack = stack[:len(stack)-1]
fmt.Println("弹出元素:", top)
}
// 队列的实现
queue := make([]int, 0)
// 入队
queue = append(queue, 1, 2, 3)
// 出队
if len(queue) > 0 {
front := queue[0]
queue = queue[1:]
fmt.Println("取出元素:", front)
}6.3 二维切片
Go 支持动态的二维切片(类似于变长的二维数组):
// 创建一个3行的二维切片
matrix := make([][]int, 3)
// 为每行分配不同的长度
for i := range matrix {
matrix[i] = make([]int, i+1)
}
// 填充数据
for i := range matrix {
for j := range matrix[i] {
matrix[i][j] = i*10 + j
}
}
// 输出:[[0] [10 11] [20 21 22]]
fmt.Println(matrix)