面试官:请问 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,或者将修改后的切片作为返回值返回。