数组(Array)和切片(Slice)的区别?
在 Go 语言中,数组(Array)和切片(Slice)是两个紧密相关但行为截然不同的概念。
简单总结:数组是固定长度的值类型,切片是动态长度的引用类型(底层是对数组的抽象)。
以下是详细的区别对比:
1. 长度与类型定义
- 数组(Array):长度是类型的一部分。
[5]int和[10]int是完全不同的两种数据类型,不能互相赋值。- 数组一旦定义,长度固定,不可修改。
- 切片(Slice):长度不是类型的一部分。
[]int可以表示任意长度的整数序列。- 切片的长度是动态的,可以随着元素的添加(
append)而自动扩容。
2. 内存传递(值类型 vs 引用类型)
这是两者最核心的区别,决定了它们在函数传参和赋值时的行为。
- 数组是值类型(Value Type):
- 当你将一个数组赋值给另一个变量,或者作为参数传递给函数时,会发生完全拷贝(Deep Copy)。
- 修改副本不会影响原数组。
- 缺点:如果数组很大,传参会消耗大量内存和性能。
- 切片是引用类型(Reference Type):
- 切片本质上是一个轻量级的结构体(Header),包含三个字段:指向底层数组的指针、长度(len)、容量(cap)。
- 赋值或传参时,只拷贝这个轻量级的 Header(浅拷贝)。
- 修改切片中的元素,会影响到底层数组,因此其他共享该底层数组的切片也会受到影响。
3. 初始化与声明
- 数组: 需要指定长度(或使用
...让编译器推断)。govar a [5]int // 声明一个长度为5的数组,全为0 b := [3]int{1, 2, 3} // 初始化 c := [...]int{1, 2, 3} // 编译器自动推断长度为3 - 切片: 不需要指定长度,或者使用
make。govar s []int // 声明一个切片,默认为 nil s1 := []int{1, 2, 3} // 字面量初始化 s2 := make([]int, 5, 10) // 使用 make 创建:长度5,容量10 s3 := arr[1:3] // 从数组 arr 切分出来
4. 扩容与增删
- 数组: 不支持扩容,不支持动态添加或删除元素。
- 切片: 支持使用内置函数
append()添加元素。- 当切片容量不足时,Go 运行时会自动分配一个新的、更大的底层数组,并将原数据拷贝过去(扩容机制)。
5. 比较(Comparison)
- 数组: 如果数组的元素类型是可比较的(如 int, string),那么数组也是可比较的。可以使用
==或!=比较两个数组的内容。 - 切片: 不可比较。切片只能和
nil进行比较。不能使用==判断两个切片是否相等(如果需要比较,需使用reflect.DeepEqual或循环遍历)。
总结对比表
| 特性 | 数组 (Array) | 切片 (Slice) |
|---|---|---|
| 长度 | 固定,长度是类型的一部分 | 动态,长度不影响类型 |
| 类型定义 | [n]T (如 [5]int) |
[]T (如 []int) |
| 赋值/传参 | 值拷贝 (复制整个数组数据) | 引用传递 (复制指针、长度、容量) |
| 扩容 | 不支持 | 支持 append 自动扩容 |
| 比较 | 支持 == (若元素可比较) |
不支持 (只能与 nil 比较) |
| 底层结构 | 连续的内存块 | 结构体:{ptr, len, cap} |
代码示例
1. 数组的值拷贝行为:
go
func main() {
a := [3]int{1, 2, 3}
b := a // 发生完全拷贝
b[0] = 100
fmt.Println(a) // 输出: [1 2 3] (原数组未变)
fmt.Println(b) // 输出: [100 2 3]
}
2. 切片的引用行为:
go
func main() {
a := []int{1, 2, 3}
b := a // 仅拷贝 Header,指向同一个底层数组
b[0] = 100
fmt.Println(a) // 输出: [100 2 3] (原切片也被修改了)
fmt.Println(b) // 输出: [100 2 3]
}
什么时候用哪个?
- 99% 的情况使用切片。切片灵活、高效、功能强大,是 Go 语言处理序列数据的标准方式。
- 使用数组的情况:
- 当你确切知道数据的长度,且长度很小(例如 MD5 的返回值是
[16]byte)。 - 为了避免内存分配开销(数组在栈上分配,切片底层数组通常在堆上)。
- 需要对数据布局进行精细控制时。
- 当你确切知道数据的长度,且长度很小(例如 MD5 的返回值是
右滑查看面试常问