面试官:请问 Go 语言中数组(Array)和切片(Slice)的区别是什么?
面试回答
“在 Go 语言中,数组和切片最核心的区别在于长度是否可变以及传递方式。
首先,数组是定长的,它的长度在声明时就已经确定,并且是类型的一部分。比如 [3]int 和 [5]int 是完全不同的类型。而切片是动态的,它的长度可以随时改变,底层其实是对数组的一个封装。
其次,在赋值和函数传参时,Go 语言中只有值传递。传递数组时,会进行完整的内存拷贝,如果数组很大,性能开销会很高。而传递切片时,拷贝的仅仅是切片底层的结构体(包含指针、长度和容量),因为指针指向同一个底层数组,所以表现出了引用语义,底层数组是共享的,效率非常高。
在实际开发中,我们绝大多数情况下使用的都是切片,因为它的动态扩容特性更加灵活。只有在明确知道数据长度固定,或者需要作为 map 的 key 时,才会考虑使用数组。”
系统讲解
核心对比
| 特性 | 数组 (Array) | 切片 (Slice) |
|---|---|---|
| 长度 | 固定,声明后不可改变 | 动态,可随时通过 append 扩容 |
| 类型 | 长度是类型的一部分 ([3]int ≠ [5]int) | 长度不是类型的一部分 ([]int) |
| 传递方式 | 值传递,拷贝整个数组 | 值传递,仅拷贝切片头结构体(表现出引用语义) |
| 底层结构 | 连续的内存块 | 包含指针、长度 (len) 和容量 (cap) 的结构体 |
| 初始化 | [3]int{1, 2, 3} 或 [...]int{1, 2, 3} | make([]int, 0, 5) 或 []int{1, 2, 3} |
底层原理
切片 (Slice) 的底层结构 reflect.SliceHeader 如下:
type SliceHeader struct {
Data uintptr // 指向底层数组的指针
Len int // 切片当前包含的元素个数
Cap int // 底层数组的容量(从指针位置到数组末尾的长度)
}因为切片是对底层数组的视图(View),所以多个切片可以共享同一个底层数组。当修改切片中的元素时,底层数组也会被修改。
代码示例
// 1. 数组:值传递与类型差异
func testArray() {
arr1 := [3]int{1, 2, 3}
arr2 := arr1 // 发生值拷贝
arr2[0] = 99
fmt.Println(arr1) // 输出: [1 2 3] (原数组未改变)
fmt.Println(arr2) // 输出: [99 2 3]
// var arr3 [4]int = arr1 // 编译报错:cannot use arr1 (type [3]int) as type [4]int
}
// 2. 切片:值传递(拷贝切片头)与动态扩容
func testSlice() {
slice1 := []int{1, 2, 3}
slice2 := slice1 // 仅拷贝 SliceHeader,底层数组共享
slice2[0] = 99
fmt.Println(slice1) // 输出: [99 2 3] (原切片被改变)
fmt.Println(slice2) // 输出: [99 2 3]
// 动态扩容
slice1 = append(slice1, 4)
fmt.Println(len(slice1), cap(slice1)) // 容量不足时会自动扩容
}常见追问
追问 1:切片的扩容机制是怎样的?
在 Go 1.18 之前,当切片容量小于 1024 时,每次扩容容量翻倍(newcap = oldcap * 2);当容量大于等于 1024 时,每次增加 25%(newcap = oldcap * 1.25)。
从 Go 1.18 开始,为了让扩容更加平滑,阈值调整为 256。当容量小于 256 时,依然是翻倍;大于等于 256 时,每次增加的容量会随着当前容量的增大而逐渐减小,大致公式为 newcap = oldcap + (oldcap + 3*256) / 4。
追问 2:切片作为函数参数传递时,在函数内 append 会影响外部吗?
不会直接影响外部切片的长度。因为 Go 中只有值传递,传递切片时,拷贝了底层数组的指针、长度(len)和容量(cap)。
如果在函数内修改已有元素,外部可见;但如果使用 append 添加了新元素,由于外部切片的 len 没有改变,外部依然无法访问到新添加的元素。如果 append 触发了扩容,内部切片会指向一块新的内存,此时内部和外部的底层数组就完全脱钩了。如果需要修改外部切片,应该传递切片的指针 *[]int,或者将修改后的切片作为返回值返回。