Go语言数组与切片详解
Contents
在 Go 语言中,数组(Array)和切片(Slice)是最常用的顺序容器。它们看似相似,却有本质上的区别:数组是值类型、长度固定;切片是引用类型、长度可变。本文将从定义、内存模型、函数传参/返回、使用场景等方面进行详细总结。
一、数组(Array)
1. 定义与声明
数组是 定长的元素集合,长度是类型的一部分:
var arr1 [3]int // 默认初始化为 [0,0,0]
arr2 := [3]int{1, 2, 3} // 显式初始化
arr3 := [...]int{1, 2, 3, 4} // 由编译器推导长度2. 特点
- 长度固定,一旦声明,长度不能改变。
- 值类型,赋值和传参时会复制整个数组。
[3]int和[4]int是完全不同的类型。
3. 使用示例
func modifyArray(a [3]int) {
a[0] = 100 // 修改的是副本
}
func main() {
arr := [3]int{1, 2, 3}
modifyArray(arr)
fmt.Println(arr) // [1 2 3] 未改变
}4. 内存模型
- 数组的所有元素连续存储在栈或堆上。
- 传参/赋值时,会整体拷贝。
二、切片(Slice)
1. 定义与声明
切片是对数组的 动态视图,本质上是一个 slice header,包含三个字段:
ptr:指向底层数组的指针len:当前切片的长度cap:切片的容量(从ptr到底层数组末尾的元素个数)
var s1 []int // nil 切片,len=0, cap=0
s2 := []int{1, 2, 3} // 字面量声明
s3 := make([]int, 5, 10) // len=5, cap=10
s4 := s2[1:3] // 基于数组/切片的切片2. 特点
- 引用类型,传参和赋值只会拷贝 slice header,不会拷贝底层数组。
- 长度和容量可变,可以
append。 - 多个切片可能共享同一底层数组。
3. 使用示例
func modifySlice(s []int) {
s[0] = 100 // 修改底层数组,外部可见
}
func main() {
s := []int{1, 2, 3}
modifySlice(s)
fmt.Println(s) // [100 2 3] 已改变
}4. 内存模型
slice header {
ptr -> 底层数组
len
cap
}多个切片可能共享同一个底层数组。
三、数组与切片在函数传参/返回的区别
1. 数组
- 值类型,传参和返回时会完整拷贝。
- 适合小规模、固定大小的数据。
func returnArray() [3]int {
return [3]int{1, 2, 3} // 拷贝整个数组返回
}2. 切片
- 引用类型,传参和返回只会拷贝 header。
- 修改切片元素会影响外部。
- append 时可能触发 扩容,新分配底层数组,导致与原切片分离。
func returnSlice() []int {
return []int{1, 2, 3} // 返回引用,轻量
}四、数组 vs 切片对比表
| 特性 | 数组 [N]T | 切片 []T |
|---|---|---|
| 类型 | 值类型 | 引用类型(header 包含 ptr,len,cap) |
| 长度 | 固定,编译期确定 | 可变,运行期调整 |
| 内存开销 | 存储整个数组 | 仅存储 header(24 字节,64 位机上) |
| 传参/返回 | 拷贝整个数组 | 拷贝 header,轻量 |
| 修改是否影响外部 | 否 | 是(共享底层数组) |
| 常用场景 | 固定大小、矩阵、缓存块 | 动态序列、切片操作、集合处理 |
五、使用场景与最佳实践
数组
- 适合存放固定大小的数据,如
[16]byte、矩阵、环形缓冲区。 - 避免频繁拷贝大数组,可以用指针
*[N]T。
- 适合存放固定大小的数据,如
切片
- 适合动态集合和大部分日常开发场景。
- 避免切片越界,注意扩容时可能导致与原切片“断开”。
- 推荐通过
make提前设置合理的容量,减少扩容开销。
六、总结
- 数组:定长、值类型、拷贝成本高,更像 C 语言的数组。
- 切片:变长、引用类型、使用灵活,是 Go 中实际开发的首选。
- 牢记一句话:在 Go 中,数组偏底层,切片才是主角。