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