一、快速记忆(先记核心)

方面 数组 切片
本质 固定长度的值 指向底层数组的视图
传参时 整个复制 只复制描述信息(24字节)
函数内改元素 不影响外面 影响外面
函数内改长度 不允许(长度固定) 外面看不到(需返回)

二、详细对比(4个方面)

1️⃣ 内存和性能

数组传参:复制全部数据

func consumeArray(arr [1000000]int) {  // 100万个int,约8MB
    // 函数调用时会完整复制这8MB数据
}

func main() {
    huge := [1000000]int{}
    consumeArray(huge)  // 慢!复制8MB
}

切片传参:只复制小纸条

func consumeSlice(slice []int) {  // 只复制24字节(指针+长度+容量)
    // 不管底层数组多大,都只复制24字节
}

func main() {
    huge := make([]int, 1000000)  // 底层8MB数组
    consumeSlice(huge)  // 快!只复制24字节
}

结论:大数组用切片传参,性能好得多。

2️⃣ 修改内部元素的影响

package main

import "fmt"

// 数组:改副本,不影响外面
func modifyArray(arr [3]int) {
    arr[0] = 999
}

// 切片:改原数组,影响外面
func modifySlice(slice []int) {
    slice[0] = 999
}

func main() {
    // 数组测试
    arr := [3]int{1, 2, 3}
    modifyArray(arr)
    fmt.Println("数组传参后:", arr)  // [1 2 3] 没变!
    
    // 切片测试
    slice := []int{1, 2, 3}
    modifySlice(slice)
    fmt.Println("切片传参后:", slice)  // [999 2 3] 变了!
}

输出:

数组传参后: [1 2 3]
切片传参后: [999 2 3]

3️⃣ 修改长度(增删元素)

这是最坑的地方,细看:

package main

import "fmt"

// 尝试在函数内增加元素
func addElement(s []int) {
    s = append(s, 100)  // 增加一个元素
    fmt.Println("函数内部:", s)  // [1 2 3 100]
}

func main() {
    slice := []int{1, 2, 3}
    addElement(slice)
    fmt.Println("函数外部:", slice)  // [1 2 3] 没变!
}

为什么?

因为切片有3个信息:

  • ptr(指针):指向底层数组

  • len(长度):当前有多少元素

  • cap(容量):最多能装多少

传参时复制了这3个信息,所以:

  • 函数内的 len 是3,append后变成4(但改的是副本)

  • 外面的 len 仍然是3

解决方案:返回新切片

func addElement(s []int) []int {
    s = append(s, 100)
    return s  // 返回新的切片
}

func main() {
    slice := []int{1, 2, 3}
    slice = addElement(slice)  // 接收返回值
    fmt.Println(slice)  // [1 2 3 100] 成功!
}

4️⃣ 函数签名(类型系统)

// 数组:长度是类型的一部分!
func process(arr1 [3]int) {}    // 只能接收长度3的数组
func process(arr2 [5]int) {}    // 这是不同的类型

// 切片:长度不是类型的一部分
func process(slice []int) {}     // 可以接收任何长度的int切片

func main() {
    arr3 := [3]int{1, 2, 3}
    arr5 := [5]int{1, 2, 3, 4, 5}
    
    process(arr3)  // ✅ 可以
    process(arr5)  // ❌ 编译错误!类型不匹配
    
    slice := []int{1, 2, 3}
    process(slice)  // ✅ 可以
    slice2 := []int{1, 2, 3, 4, 5}
    process(slice2)  // ✅ 也可以
}

结论:切片更灵活,数组太死板。

三、特殊情况:切片扩容

当append时容量不够,会重新分配底层数组:

func modifyWithAppend(s []int) {
    s = append(s, 4)  // 如果容量够,继续用原数组
    s[0] = 100
}

func main() {
    // 情况1:容量够
    slice1 := make([]int, 3, 10)  // 长度3,容量10
    slice1[0], slice1[1], slice1[2] = 1, 2, 3
    modifyWithAppend(slice1)
    fmt.Println("容量够:", slice1)  // [100 2 3] 元素变了,但长度还是3
    
    // 情况2:容量不够
    slice2 := []int{1, 2, 3}  // 长度3,容量3(不够了)
    modifyWithAppend(slice2)
    fmt.Println("容量不够:", slice2)  // [1 2 3] 完全没变!
    // 因为append时重新分配了新数组,跟外面的数组没关系了
}

四、如何选择?(实际开发建议)

你的需求 选择 理由
日常开发,数据会变动 切片 99%的情况都用切片
数组大小固定不变(如坐标点) 数组或切片均可 但切片更灵活
函数需要修改长度 切片 + 返回新切片 这是标准做法
想保护数据不被函数修改 数组 或 复制切片 用 copy() 复制切片
性能极端敏感,要避免扩容 切片 + make预分配容量 make([]int, 0, 1000)
函数间传递超大数据 切片(必须是切片) 避免复制开销

五、实用技巧

技巧1:想保护切片不被修改

func protectData(original []int) {
    // 复制一份
    copy := make([]int, len(original))
    copyData := append([]int{}, original...)  // 更简洁的复制
    
    // 让其他函数用副本
    dangerousFunction(copyData)
}

技巧2:明确需要修改切片

func modifySlice(s []int) []int {
    // 修改...
    s = append(s, 999)
    return s  // 始终返回
}

// 调用时
mySlice = modifySlice(mySlice)

技巧3:面试常问的坑

func test(s []int) {
    s = append(s, 4)
    s[0] = 100
}

func main() {
    s := []int{1, 2, 3}
    test(s)
    fmt.Println(s)  // 请问输出什么?
}
// 答案:[1 2 3] 
// 因为append后s变了,但外面的s没变

总结口诀

数组笨重传全部,改了外面不关注。
切片轻巧传地址,增删改要懂规矩。
改元素时内外通,要变长度得返回送。

最后记住:实际写代码,99%都用切片,数组只有在极特殊场景(比如需要固定长度类型的key、极小数组)才用。

更多推荐