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 个核心步骤:

  1. 检查是否需要“一步到位”
    如果当前切片需要追加的元素非常多,导致所需的最小总长度(newLen)直接超过了原容量的 2 倍,Go 会放弃常规估算,直接将新容量设定为你需要的实际大小(newLen),避免反复拷贝。
  2. 按当前容量大小分段计算
    如果没触发第一步的特殊情况,Go 会根据切片当前的容量(oldCap)来决定增长幅度:
    当前容量 < 256:为了减少频繁分配内存的开销,直接将容量翻倍(2倍)1。
    当前容量 ≥ 256:采用平滑过渡的公式,每次增加约 25%(即 newCap += newCap / 4),也就是大约 1.25 倍的增长1。
  3. 内存对齐
    通过前两步算出来的只是一个“理论容量”。Go 的底层内存分配器并不会严格按这个字节数来分配,而是会将所需的总字节数(理论容量 × 元素大小)向上取整到最接近的内存规格块(Size Class)。(可能会出现理论和实际容量不一的情况。)
  4. 分配新内存并拷贝数据
    确定最终容量后,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)
}
comments如果有不同意见或者补充,直接留在这里。
contact

在别处继续找到我

如果你想聊技术、设计,或者只是打个招呼。

暂未配置外部链接