1. 引言
在Go语言中,数组(Array)和切片(Slice,常被称为动态数组)是两种基础且重要的数据结构。它们都用于存储相同类型的元素序列,但在特性、用法和内存管理上有着本质区别。理解二者的差异是编写高效、正确Go程序的关键。本文将深入剖析Go数组和切片的核心概念、创建方式、内存模型以及使用场景,帮助你彻底掌握这两种数据结构。
2. 数组(Array)
2.1 定义与特性
数组是具有固定长度的、由相同类型元素组成的序列。其长度在声明时就必须确定,且在整个生命周期中不可改变。
核心特性:
- 固定长度 :长度是类型的一部分。
[5]int和[10]int是两种不同的类型。 - 值类型 :数组是值类型。当数组被赋值给一个新变量或作为参数传递时,会发生整个数组的值拷贝。
- 内存连续:元素在内存中连续存储,支持通过索引(下标)进行高效随机访问。
2.2 声明与初始化
go
// 1. 声明一个长度为5的int数组,元素初始化为零值(0)
var arr1 [5]int // [0 0 0 0 0]
// 2. 使用数组字面量初始化
arr2 := [3]int{1, 2, 3} // [1 2 3]
arr3 := [5]int{1, 2} // [1 2 0 0 0] (未指定部分为零值)
// 3. 让编译器推断长度
arr4 := [...]int{10, 20, 30, 40} // 长度推断为4
// 4. 指定索引初始化
arr5 := [5]int{1: 100, 4: 500} // [0 100 0 0 500]
2.3 基本操作
go
// 访问与修改
arr := [3]string{"Go", "Java", "Python"}
fmt.Println(arr[0]) // 输出: Go
arr[1] = "Rust"
fmt.Println(arr) // 输出: [Go Rust Python]
// 遍历数组
for i := 0; i < len(arr); i++ {
fmt.Printf("索引 %d: %s\n", i, arr[i])
}
// 使用 range 关键字
for index, value := range arr {
fmt.Printf("索引 %d: %s\n", index, value)
}
// 长度获取
length := len(arr) // 3
2.4 值类型行为示例
go
func modifyArray(arr [3]int) {
arr[0] = 999 // 只修改副本
fmt.Println("函数内数组:", arr) // [999 2 3]
}
func main() {
original := [3]int{1, 2, 3}
modifyArray(original)
fmt.Println("原数组:", original) // [1 2 3] (未被修改)
// 赋值也是值拷贝
copyArr := original
copyArr[1] = 200
fmt.Println("原数组:", original) // [1 2 3]
fmt.Println("拷贝数组:", copyArr) // [1 200 3]
}
由于是值拷贝,对于大型数组,频繁传递会产生性能开销。
3. 切片(Slice)------动态数组
3.1 定义与特性
切片是对底层数组的一个连续片段的引用。它提供了动态大小的、灵活的视图。
核心特性:
- 动态长度 :长度可变,可以按需增长(通常通过
append函数)。 - 引用类型:切片本身是一个轻量级的数据结构(包含指针、长度和容量),传递切片时传递的是这个描述符的副本,而非底层数据,因此多个切片可以共享同一个底层数组。
- 三要素 :一个切片由三部分信息描述:
- 指针(Pointer):指向底层数组中切片起始元素的地址。
- 长度(Length) :切片中当前元素的个数(
len(s)可获取)。 - 容量(Capacity) :从切片起始位置到底层数组末尾的元素个数(
cap(s)可获取)。
3.2 创建切片
go
// 1. 基于数组切片
arr := [5]int{1, 2, 3, 4, 5}
slice1 := arr[1:4] // [2 3 4] (长度3,容量4)
slice2 := arr[:3] // [1 2 3] (从开始到索引2)
slice3 := arr[2:] // [3 4 5] (从索引2到末尾)
// 2. 使用 make 函数创建(会分配底层数组)
// make([]T, length, capacity)
slice4 := make([]int, 3, 5) // 长度3,容量5,元素为[0 0 0]
slice5 := make([]string, 2) // 长度和容量均为2,元素为["", ""]
// 3. 使用切片字面量(隐式创建底层数组)
slice6 := []int{10, 20, 30} // 长度和容量均为3
var slice7 []int // 声明一个 nil 切片,长度和容量为0,指针为nil
// 4. 基于现有切片切片
slice8 := slice6[1:] // [20 30] (长度2,容量2)
3.3 基本操作与动态增长
go
// 追加元素 - append 函数
var s []int // nil切片
s = append(s, 1) // [1]
s = append(s, 2, 3, 4) // [1 2 3 4]
s = append(s, []int{5, 6}...) // 追加另一个切片
// 当追加元素超过容量时,Go会分配一个更大的新底层数组(通常是原容量的2倍,当容量<1024时),
// 将原有元素复制过去,然后追加新元素。
fmt.Printf("长度: %d, 容量: %d\n", len(s), cap(s))
// 复制切片 - copy 函数
src := []int{1, 2, 3, 4, 5}
dst := make([]int, 3)
n := copy(dst, src) // 复制元素个数
fmt.Println("复制了", n, "个元素到dst:", dst) // 复制了 3 个元素到dst: [1 2 3]
// 遍历与数组相同
for i, v := range s {
fmt.Println(i, v)
}
3.4 内存共享与陷阱
go
func main() {
arr := [5]int{1, 2, 3, 4, 5}
s1 := arr[1:4] // [2 3 4]
s2 := s1[1:3] // [3 4] (s2与s1共享底层数组arr)
s2[0] = 300 // 修改s2会影响s1和原数组
fmt.Println("s1:", s1) // [2 300 4]
fmt.Println("arr:", arr) // [1 2 300 4 5]
// 使用 append 可能导致"分离"
s3 := make([]int, 2, 3) // 长度2,容量3
s3[0], s3[1] = 10, 20
s4 := s3[:2] // s4 与 s3 共享底层数组
s4 = append(s4, 30) // 未超容量,仍在原数组操作
fmt.Println("s3:", s3) // [10 20 30] (s3[2]也被修改了!)
s4 = append(s4, 40) // 超容量,s4获得新底层数组
s4[0] = 100
fmt.Println("s4 (新数组):", s4) // [100 20 30 40]
fmt.Println("s3 (原数组):", s3) // [10 20 30] (不再受影响)
}
4. 数组 vs 切片:核心对比
| 特性 | 数组 (Array) | 切片 (Slice) |
|---|---|---|
| 长度 | 固定,编译时确定 | 动态,运行时可变 |
| 类型 | 值类型 ([n]T) |
引用类型 ([]T) |
| 传递开销 | 拷贝整个数组,可能很大 | 拷贝描述符(指针、长度、容量),很小 |
| 内存分配 | 通常在栈上(如果不大) | 底层数组在堆上,切片描述符可能在栈上 |
| 创建方式 | var arr [N]T, arr := [...]T{...} |
make([]T, len, cap), []T{...}, 基于数组/切片 |
| 零值 | 所有元素为类型零值 | nil(指针为nil,长度和容量为0) |
| 常用场景 | 固定大小的缓冲区、转换底层数据 | 99% 的集合数据存储场景 |
5. 使用场景与最佳实践
5.1 何时使用数组?
- 确切知道元素数量且永不改变 :如存储一周七天 (
[7]string)、RGB颜色 ([3]uint8)。 - 作为切片操作的底层存储:切片需要底层数组。
- 值语义很重要时:希望数据被完整拷贝,避免意外的共享修改。
- 性能敏感且大小固定的场景:数组在栈上分配,可能避免堆内存分配和GC开销。
5.2 何时使用切片?(绝大多数情况)
- 存储可变长度的元素集合:这是切片最主要的设计目的。
- 函数参数和返回值:避免大数组拷贝,传递高效。
- 动态构建数据集:如从文件、网络读取未知数量的数据。
- 实现栈、队列等数据结构。
5.3 最佳实践
-
优先使用切片:Go语言中切片是更常用、更灵活的数据结构。
-
预分配容量 :如果知道切片的大致大小,使用
make([]T, 0, capacity)预分配容量,可以减少append时的内存分配和拷贝次数。go// 不好:频繁扩容 var s []int for i := 0; i < 1000; i++ { s = append(s, i) } // 好:一次分配足够容量 s = make([]int, 0, 1000) for i := 0; i < 1000; i++ { s = append(s, i) } -
小心切片的内存泄漏 :大切片中保留一个小引用可能阻止整个大底层数组被垃圾回收。必要时对需要保留的小部分数据使用
copy。gofunc getLastNElements(bigSlice []int, n int) []int { // 直接切片返回,会持有对原大数组的引用 // return bigSlice[len(bigSlice)-n:] // 更好的做法:拷贝需要的数据 result := make([]int, n) copy(result, bigSlice[len(bigSlice)-n:]) return result } -
使用
copy进行切片克隆 :s2 := s1只是创建了新的描述符,底层数组相同。需要独立副本时:gos2 := make([]int, len(s1)) copy(s2, s1)
6. 总结
- 数组是固定长度的值类型序列,适用于元素数量已知且不变的场景,传递时产生拷贝。
- 切片是对数组的动态视图,是Go中最常用的集合类型,具有长度可变、传递高效的特点,其行为由指针、长度和容量共同决定。
- 理解切片与底层数组的共享关系是避免bug的关键。
append在容量不足时会触发重新分配,导致与原底层数组"分离"。 - 在实际编程中,应优先考虑使用切片。仅在需要固定大小、值语义或特定性能优化时使用数组。
掌握数组和切片的区别与联系,能够帮助你写出更高效、更健壮的Go代码。