Files
cs-note/hzh/GolangStar/Go语言原理/slice原理.md
T

182 lines
6.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
tags: [go, golang, go-principle, slice]
create time: 2026-06-07 15:10
---
# Slice 底层原理
## 概述
本文从 runtime 源码角度深入解析 Go Slice 的三字段结构、扩容策略(含 1.18 版本变更),以及截取、复制、append 等操作背后的内存行为。Slice 是 Go 中最常用的数据结构之一,理解其底层机制能有效避免常见的"切片共享数组"陷阱。
> [!question] ❓ 思考
> 为什么 `arr2 := arr1[1:3]` 之后修改 `arr2[0]` 会影响 `arr1`?Slice 赋值到底是深拷贝还是浅拷贝?如果容量满了再 append,Go 会怎么做?
## 正文
### 一、Slice 的结构:一个指针 + 两个长度
Slice 在 runtime 层面的定义非常简洁:
```go
// src/runtime/slice.go
type slice struct {
array unsafe.Pointer // 指向底层数组
len int // 当前已使用的元素个数
cap int // 底层数组的总容量
}
```
三个字段共 24 字节(64 位系统)。用一个图来描述:
```mermaid
graph LR
S["slice struct<br/>array | len=3 | cap=5"] --> A["底层数组<br/>[1][2][3][4][5]"]
style S fill:#e1f5fe
style A fill:#fff9c4
```
| 字段 | 含义 | 示例值 |
|------|------|--------|
| `array` | 指向底层连续内存的指针 | `0xc00001a180` |
| `len` | 可访问的元素数量 | `3` |
| `cap` | 底层数组总大小(不含偏移) | `5` |
> [!tip] 💡 理解要点
> `cap >= len` 始终成立。`cap - len` 表示还能追加多少个元素而不触发扩容。
### 二、Slice 的三种初始化方式
```go
// 1. 从已有切片截取
s1 := s[low : high : max]
// 2. 字面量初始化
s2 := []int{1, 2, 3}
// 3. make 创建
s3 := make([]int, 3) // len=3, cap=3
s4 := make([]int, 3, 5) // len=3, cap=5
```
注意截取操作 `s[low : high : max]` 中:
- 新切片的 `len = high - low`
- 新切片的 `cap = max - low`
- 新切片与原切片**共享同一块底层数组**
### 三、截取与赋值:共享底层数组
这是初学者最容易踩坑的地方——截取和赋值都不会复制底层数据:
```go
src := []int{1, 2, 3, 4, 5}
dst := src[1:3] // dst 指向 src 的底层数组
dst[0] = 100 // src 变成了 [1, 100, 3, 4, 5]
```
这是因为 slice 赋值实际上只是复制了三个字段(24 字节),其中 `array` 指针指向同一片内存区域:
```mermaid
flowchart LR
src["slice[src]<br/>array→▲"] --> A["底层数组<br/>[1][100][3][4][5]"]
dst["slice[dst]<br/>array→▲"] --> A
style A fill:#fff9c4
```
> [!warning] ⚠️ 常见陷阱
> 函数间传递 slice 时,接收方对元素的修改会影响调用方。如果需要完全独立的副本,必须用 `copy()` 或手动分配新切片。
#### 如何真正复制一个 slice
```go
src := []int{1, 2, 3, 4, 5}
dst := make([]int, len(src))
copy(dst, src) // 真正的独立副本
dst[0] = 100 // src 不受影响
```
### 四、Append 与扩容策略
#### 4.1 Append 的行为
`append` 的本质是对 slice 结构体的**值拷贝**——每次调用都产生一个新的 slice 结构体(但共享底层数组),然后写入新元素:
```go
arr1 := make([]int, 0, 4)
arr1 = append(arr1, 1) // arr1=[1]
arr2 := append(arr1, 2) // arr2=[1,2]
arr3 := append(arr1, 3) // arr3=[1,3] — arr2 被覆盖了!
```
为什么 `arr2` 变成了 `[1, 3]`?因为 `arr1`、`arr2`、`arr3` 共享同一底层数组,`arr3` 的写入覆盖了 `arr2` 写入的位置。
#### 4.2 扩容公式
当 `len == cap` 时,`append` 需要分配更大的底层数组。扩容策略经历了版本变化:
**Go 1.17 及之前:**
```mermaid
flowchart TD
Start["cap < 1024?"] -->|是| Doubles["newCap = oldCap * 2"]
Start -->|否| Quarter["newCap = oldCap + oldCap/4"]
Doubles --> Check{"newCap < 需要的容量?"}
Quarter --> Check
Check -->|是| Doubles2["newCap = newCap * 2"]
Check -->|否| Final["newCap = 需要的容量"]
Doubles2 --> Final
```
**Go 1.18 及之后(更平滑的增长曲线):**
```
newCap = oldCap + (oldCap + 3*256) / 4
```
这个公式让小容量时接近翻倍,大容量时逐渐趋近于 1.25 倍,避免了大切片扩容时的内存浪费。
> [!note] 📝 源码要点
> 扩容逻辑位于 `src/runtime/slice.go` 的 `growslice` 函数。它会根据元素类型(是否包含指针)选择不同的内存分配策略,并在某些情况下直接复用旧数组(当容量增长不超过 2 倍时)。
#### 4.3 growslice 核心流程
```go
// src/runtime/slice.go (伪代码简化)
func growslice(et *_type, old slice, cap int) slice {
if et.ptrdata == 0 {
// 无指针类型:一次 mallocgc 分配 hchan + buf
newBuf = mallocgc(newSize, nil, true)
memmove(newBuf, old.array, old.len) // 拷贝旧数据
} else {
// 有指针类型:分别分配
newBuf = mallocgc(newSize, et, true)
memmove(newBuf, old.array, old.len)
}
return slice{array: newBuf, len: old.len, cap: newCap}
}
```
关键点:
- 扩容一定会**分配新内存并拷贝旧数据**
- 如果元素包含指针,GC 会在拷贝后的新区间上重新扫描
- 当请求的新容量不超过旧容量的 2 倍时,Go 会尝试直接扩展原有内存(零拷贝扩容)
### 五、性能建议
1. **预分配容量**:已知最终大小的场景,用 `make([]T, 0, n)` 避免多次扩容
2. **截取慎用**:截取后原数组无法被 GC 回收整片内存(只要有一个切片引用着)
3. **深拷贝需求**:用 `copy()` 而非切片赋值,避免隐式共享
## 小结
- Slice 是 `(ptr, len, cap)` 三字段结构,本质是一个"窗口"指向底层数组
- 截取和赋值都是浅操作,共享底层数组;深拷贝需用 `copy()`
- 扩容策略因版本而异:1.17 前是小容量翻倍/大容量加 1/4,1.18+ 用平滑公式
- 预分配容量能显著减少扩容带来的开销
## 关联笔记
- [[hzh/GolangStar/Go语言基础/Go语言数组与切片]] — Slice 的基础用法
- [[hzh/GolangStar/Go语言原理/string原理]] — String 的底层结构与转换
- [[hzh/GolangStar/Go面试题库/Slice面试题]] — Slice 相关高频面试题