essay
关于数组和切片
#go
关于数组和切片
在 Go 语言中,数组(Array)和切片(Slice)都是用来存储同类型数据的集合,但它们的特性和使用场景有着本质的区别。简单来说,数组是固定长度的,而切片是动态长度的。
| 特性 | 数组 (Array) | 切片 (Slice) |
|---|---|---|
| 长度 | 固定,定义后不可变 | 动态,可自动扩容 |
| 类型 | 值类型(赋值/传参会完整拷贝) | 引用类型(底层共享数据) |
| 声明 | 必须指定长度,如 int |
不指定长度,如 []int |
| 底层结构 | 连续的内存空间 | 包含指针、长度(len)、容量(cap) |
1. 数组的声明与初始化:
// [长度]类型,长度是数组类型一部分,不可修改
var arr [5]int
a := [3]int{1,2,3}
b := [...]int{1,2,3,4} // 编译器自动推导长度2. 数组的核心特性:
- 长度固定: 数组的长度是类型的一部分。这意味着 int 和 int 是两种完全不同的类型,不能互相赋值2。
- 值类型: 当把一个数组赋值给另一个变量,或者作为参数传递给函数时,Go 会完整拷贝整个数组。修改副本不会影响原数组2。
func change(arr [3]int) {
arr[0] = 999 // 修改副本,不影响原数组
}
func main(){
a := [3]int{1,2,3}
change(a)
// a 仍为 [1,2,3]
}1. 切片的创建与初始化
// 1. 使用字面量直接创建(最常用)
s1 := []int{1, 2, 3}
// 2. 使用 make 函数(可以预设长度和容量)
s2 := make([]int, 5) // 长度5,容量5
s3 := make([]int, 3, 10) // 长度3,容量10
// 3. 从数组或其他切片中截取
arr := int{1, 2, 3, 4, 5}
s4 := arr[1:4] // 截取索引1到3的元素,结果为 [2 3 4]2. 切片的核心特性:
- 动态长度: 切片的长度可以随着元素的增加(append)而自动增长。
- 引用类型: 切片内部其实是一个包含指针(Pointer)、长度(Len)、容量(Cap)的三元组结构体1。
指针:指向底层数组的起始位置。- 长度 (len):切片中当前实际拥有的元素个数。
- 容量 (cap):从切片起始位置到底层数组末尾,最多能容纳的元素个数1。
3. append 扩容机制
当切片容量不足时,扩容会按顺序经过以下 4 个核心步骤:
- 检查是否需要“一步到位”
如果当前切片需要追加的元素非常多,导致所需的最小总长度(newLen)直接超过了原容量的 2 倍,Go 会放弃常规估算,直接将新容量设定为你需要的实际大小(newLen),避免反复拷贝。 - 按当前容量大小分段计算
如果没触发第一步的特殊情况,Go 会根据切片当前的容量(oldCap)来决定增长幅度:
当前容量 < 256:为了减少频繁分配内存的开销,直接将容量翻倍(2倍)1。
当前容量 ≥ 256:采用平滑过渡的公式,每次增加约 25%(即 newCap += newCap / 4),也就是大约 1.25 倍的增长1。 - 内存对齐
通过前两步算出来的只是一个“理论容量”。Go 的底层内存分配器并不会严格按这个字节数来分配,而是会将所需的总字节数(理论容量 × 元素大小)向上取整到最接近的内存规格块(Size Class)。(可能会出现理论和实际容量不一的情况。) - 分配新内存并拷贝数据
确定最终容量后,Go 会在内存中开辟一块全新的、更大的底层数组,将旧数组中的所有元素浅拷贝(Shallow Copy)到新数组中,然后追加新元素,最后返回指向新数组的新切片。
所以,函数内 append 开辟新底层数组,形参 s 指向新数组;
外面原切片完全不受任何影响。
func main() {
s1 := []int{1, 2, 3, 4}
modify(s1)
fmt.Println(s1) //还是返回[1 2 3 4]
}
func modify(s []int) {
s = append(s, 5)
}