一、数组(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,不够,会扩容
扩容意味着:
- 新建一个更大的底层数组
- 把旧数组数据复制进去(拷贝)
- 切片指向新的数组
这是一个 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 可以继续 append 到 5 个元素
但如果你不想让它增长(不想共享底层):
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 本身就很好。


