go语言-切片与数组的区别

一、数组(array)和切片(slice)


✅核心区别一览表

项目

数组(array)

切片(slice)

结构组成

固定长度 + 连续内存

动态长度 + 指向底层数组

长度

固定(类型的一部分)

可变(长度和容量分开)

类型定义

[3]int、[5]string 各是不同类型

[]int、[]string

值传递

拷贝整个数组

拷贝切片结构(底层数组共享)

内存分配

存储在栈或数据段

指针指向底层 array

使用场景

几乎不用(除非特定优化)

99% 的情况都用 slice

作为函数参数

会复制整个数组

效率高,常用


1️⃣数组(array)是值类型,长度固定

var a [3]int
  • [3]int 是一个类型,[4]int 又是另外一种类型。
  • 赋值、传参时 会复制整个数组

示例:

func testArr(x [3]int) {
    x[0] = 100
}

func main() {
    a := [3]int{1, 2, 3}
    testArr(a)
    fmt.Println(a) // [1 2 3] —— 不会变
}

2️⃣切片(slice)是引用类型 + 动态扩容

切片由三个部分组成:

type slice struct {
    ptr *T   // 指向底层数组
    len int
    cap int
}

切片是对底层数组的“视图”,多个切片可以共享底层数组。

示例:

func testSlice(s []int) {
    s[0] = 100
}

func main() {
    a := []int{1, 2, 3}
    testSlice(a)
    fmt.Println(a) // [100 2 3] —— 会变
}

切片扩容:

  • 当追加超出 cap 时,会新建底层数组并复制原数据。

3️⃣关键区别:值传递 vs 引用底层数组

数组作为值传递:

b := a // 整个数组拷贝

切片作为值传递:

s2 := s // 只拷贝 slice header,底层数组共享

4️⃣ 实际开发中怎么用?

  • 数组:几乎不用
    除了固定长度结构(如 [16]byte 的 UUID)才会用。
  • 切片:Go 项目中最常用的容器结构
    动态增长 + 引用开销小 + 内置操作方便。

5️⃣ 面试常问点(关键考点)

❗切片扩容后会不会影响原切片?

  • cap 没满时共享底层数组 → 修改会相互影响
  • cap 满了后 append 会分配新数组 → 修改不会影响原切片

示例:

s := []int{1, 2, 3}
s2 := s
s2 = append(s2, 4) // cap=3,会扩容!

s2[0] = 100
fmt.Println(s)  // [1 2 3]
fmt.Println(s2) // [100 2 3 4]

6️⃣ 小总结

✅ 数组 array

  • 类型带长度
  • 值类型
  • 传递和赋值都会复制整个数组
  • 基本不用

✅ 切片 slice

  • 引用类型
  • 动态增长
  • 传参不会复制数据(只复制 slice header)
  • Go 中的主力容器

二、cap的作用

cap(容量)在 Go 的切片中扮演着超级关键的角色,它直接影响:

  • 性能
  • 内存分配
  • 扩容行为
  • 与底层数组是否共享

下面我用最清晰的方式告知你 cap 的作用


✅cap 的核心作用:决定切片 append 时会不会重新分配底层数组

✔当 append 后不超过 cap → 不会重新分配(超级快)

s := make([]int, 2, 5)
fmt.Println(cap(s)) // 5

s = append(s, 10) // 不超过 cap=5

结果:

  • 不会新建底层数组
  • 切片继续引用同一个底层数组
  • 性能极高

✔当 append 导致长度 > cap → 触发扩容(开销大)

s := make([]int, 2, 2)
s = append(s, 10) // cap=2,不够,会扩容

扩容意味着:

  1. 新建一个更大的底层数组
  2. 把旧数组数据复制进去(拷贝)
  3. 切片指向新的数组

这是一个 O(n) 的操作(有性能开销)。


总结一句话:cap 决定 append 的效率和底层数组是否变动

  • cap 足够 → 零拷贝,引用不变,性能好
  • cap 不够 → 必须扩容,底层数组会变

cap 的 3 个实际作用(超级实际)

1️⃣ 提高 append 效率(避免频繁扩容)

如果你预先知道大致要多少元素:

s := make([]int, 0, 1000)  // 提前分配好容量

这样中途会几乎不扩容,append 会超级快。


2️⃣ 控制切片之间是否共享底层数组

看一个常见坑:

a := make([]int, 2, 3)
b := a[:2]

b = append(b, 100) // 不超过cap=3

由于 cap 够用,所以:

  • a 和 b 的底层数组依旧是同一个
  • b 修改会影响 a

如果 cap 不够:

b = append(b, 200, 300) // 超过 cap=3

→ b 会扩容,拥有新数组
→ a 和 b 完全分离

cap 是否够用,决定切片是否分离。


3️⃣ 控制切片能否向后继续增长(用于截断)

例如:

s := []int{1, 2, 3, 4, 5}
s2 := s[:3] // len=3 cap=5

此时:

s2 可以继续 append5 个元素

但如果你不想让它增长(不想共享底层):

s3 := s[:3:3] // len=3 cap=3

此时:

任何 append 都会触发扩容 → 必然生成新数组

通过 cap 可以人为控制切片的扩展行为。


一句总结:cap 的作用

cap 决定切片的最大可扩展空间,影响 append 是否扩容、内存分配效率、以及切片之间是否共享底层数组。

这是切片性能和行为的关键控制因素。


三、初始化一个切片最好用make来吗?

✅大多数情况下,初始化切片的确 推荐用make。

但不是所有情况都要用 make,我给你按实际开发做一个最清晰的分类。


什么时候应该用 make?(强烈推荐场景)

✅1. 你知道大致需要多少容量(最常见)

例如你要构造一个列表、有循环 append:

s := make([]int, 0, 1000)

优点:

  • 预分配容量,减少扩容次数
  • 性能更好(避免多次内存拷贝)
  • append 效率高

这是 Go 官方推荐的最佳实践之一。


✅2. 你需要一个空切片(不是 nil)

s := make([]int, 0)

区别:

var s []int   // nil 切片
make([]int,0) // 空切片

多数函数返回空切片时提议用空切片,而不是 nil。


✅3. 想控制切片初始长度

s := make([]int, 5) // len=5 cap=5

创建一个固定长度、可直接访问的切片。


什么时候不需要 make?(并不是所有情况)

❌1. 用字面量初始化时,不需要 make

s := []int{1, 2, 3}

效果超级好,不需要 make。


❌2. 作为返回值,直接 append 就可以

许多函数里写:

var s []int
for i := 0; i < n; i++ {
    s = append(s, i)
}
return s

如果你不知道最终容量,var s []T 就够用了,也没性能问题。


❌3. 只想定义一个 nil 切片

var s []int

这是nil 切片,并没有问题,而且节省内存。


总结:什么时候要用 make?

✔需要预分配容量 → 用 make(性能最好)

✔需要空切片 → 用 make([]T,0)

✔需要指定长度 → 用 make(len,cap)

❌ 字面量初始化 → 不用 make

❌ 想要 nil 切片 → 不用 make

❌ 结果都用 append → 不必定要 make


一句话总结

如果未来会 append 大量数据,用 make 预分配 cap 是最佳实践;
其他情况字面量或 var s []T 本身就很好。


© 版权声明

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
none
暂无评论...