Initial commit
This commit is contained in:
@@ -0,0 +1,465 @@
|
||||
# Go 语言切片详解:从基础到实践
|
||||
|
||||
在 Go 语言中,**切片(Slice)** 是对数组一个连续片段的引用。它是 Go 中最核心、最灵活的数据结构,几乎在所有场景下都取代了数组。理解切片是掌握 Go 语言的关键一步,它深刻影响着程序的性能和内存管理。
|
||||
|
||||
---
|
||||
|
||||
## 1. 切片基础概念
|
||||
|
||||
切片是一个**封装了底层数组信息的结构体**,它本身不存储任何数据,只是一个“视图”或“窗口”。
|
||||
|
||||
### 核心特征
|
||||
|
||||
- ✅ **类型一致性**:所有元素必须是相同类型。
|
||||
- ✅ **动态长度**:长度可变,可以随时通过 `append` 等操作增删元素。
|
||||
- ✅ **引用类型**:赋值和传参时,传递的是切片头的**浅拷贝**,但它们指向**同一个底层数组**。修改其中一个切片的元素会影响到另一个。
|
||||
- ✅ **包含三个核心组件**:
|
||||
1. **指针 (Pointer)**:指向底层数组中切片指定开始位置的内存地址。
|
||||
2. **长度 (Length)**:切片中元素的数量,`len()` 函数获取。
|
||||
3. **容量 (Capacity)**:从切片开始位置到底层数组末尾的元素数量,`cap()` 函数获取。
|
||||
|
||||
> ⚠️ 注意:切片是否拥有自己的底层数组是不确定的。它可能与其它切片共享,也可能在扩容后获得一个新的底层数组。
|
||||
|
||||
---
|
||||
|
||||
## 2. 切片声明与初始化
|
||||
|
||||
### 示例 1:基本声明与初始化
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
func main() {
|
||||
// 方式一:使用字面量直接初始化
|
||||
s1 := []int{1, 2, 3, 4, 5}
|
||||
fmt.Printf("s1: %v, len: %d, cap: %d\n", s1, len(s1), cap(s1))
|
||||
// s1: [1 2 3 4 5], len: 5, cap: 5
|
||||
|
||||
// 方式二:使用 make 函数创建,可以指定长度和容量
|
||||
// make([]T, length, capacity)
|
||||
s2 := make([]int, 5, 10)
|
||||
fmt.Printf("s2: %v, len: %d, cap: %d\n", s2, len(s2), cap(s2))
|
||||
// s2: [0 0 0 0 0], len: 5, cap: 10
|
||||
|
||||
// 方式三:声明一个 nil 切片
|
||||
var s3 []int
|
||||
fmt.Printf("s3: %v, len: %d, cap: %d\n", s3, len(s3), cap(s3))
|
||||
fmt.Println("s3 is nil?", s3 == nil) // true
|
||||
// s3: [], len: 0, cap: 0
|
||||
// s3 is nil? true
|
||||
}
|
||||
```
|
||||
|
||||
> ✅ **nil 切片** vs **空切片**:`var s []int` 是 nil 切片,`s := []int{}` 或 `s := make([]int, 0)` 是空切片。它们 `len` 和 `cap` 都为 0,但 `nil` 切片不指向任何底层数组。在实践中,`append`、`len`、`cap` 和 `range` 对它们的操作效果完全一样。
|
||||
|
||||
---
|
||||
|
||||
### 示例 2:从数组或切片创建(切片表达式)
|
||||
|
||||
这是创建切片最常见的方式,`array[low:high:max]`。
|
||||
|
||||
- `low`:开始索引(包含)
|
||||
- `high`:结束索引(不包含)
|
||||
- `max`:设置容量(可选),`cap = max - low`
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
func main() {
|
||||
arr := [10]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
|
||||
|
||||
// s[low:high] -> len=high-low, cap=cap(arr)-low
|
||||
s1 := arr[2:5]
|
||||
fmt.Printf("s1: %v, len: %d, cap: %d\n", s1, len(s1), cap(s1))
|
||||
// s1: [2 3 4], len: 3, cap: 8
|
||||
|
||||
// 省略 low,默认为 0
|
||||
s2 := arr[:5]
|
||||
fmt.Printf("s2: %v, len: %d, cap: %d\n", s2, len(s2), cap(s2))
|
||||
// s2: [0 1 2 3 4], len: 5, cap: 10
|
||||
|
||||
// 省略 high,默认为 len(arr)
|
||||
s3 := arr[5:]
|
||||
fmt.Printf("s3: %v, len: %d, cap: %d\n", s3, len(s3), cap(s3))
|
||||
// s3: [5 6 7 8 9], len: 5, cap: 5
|
||||
|
||||
// s[low:high:max] -> len=high-low, cap=max-low
|
||||
s4 := arr[2:5:7] // 从 arr[2] 开始,长度为3,容量限制到 arr[7]
|
||||
fmt.Printf("s4: %v, len: %d, cap: %d\n", s4, len(s4), cap(s4))
|
||||
// s4: [2 3 4], len: 3, cap: 5
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 切片操作详解
|
||||
|
||||
### 示例 3:访问、修改和遍历
|
||||
|
||||
切片的访问和遍历方式与数组完全相同。
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
func main() {
|
||||
s := []string{"Go", "Java", "Python", "Rust"}
|
||||
|
||||
// 访问
|
||||
fmt.Println("第一个元素:", s[0]) // Go
|
||||
|
||||
// 修改
|
||||
s[1] = "C++"
|
||||
fmt.Println("修改后:", s) // [Go C++ Python Rust]
|
||||
|
||||
// 遍历(与数组的三种方式一致)
|
||||
for i, v := range s {
|
||||
fmt.Printf("索引 %d -> 值 %s\n", i, v)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> ⚠️ 越界访问 `s[i]` 同样会导致 `panic`。
|
||||
|
||||
### 示例 4:追加元素 `append`(核心操作)
|
||||
|
||||
`append` 是切片最重要的函数,用于向切片末尾添加元素。
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
func main() {
|
||||
s := make([]int, 0, 3) // len=0, cap=3
|
||||
fmt.Printf("初始 -> len: %d, cap: %d, ptr: %p\n", len(s), cap(s), s)
|
||||
|
||||
// 1. 容量足够,不发生扩容
|
||||
s = append(s, 1)
|
||||
s = append(s, 2)
|
||||
fmt.Printf("追加2个后 -> len: %d, cap: %d, ptr: %p\n", len(s), cap(s), s)
|
||||
|
||||
// 2. 容量不足,发生扩容
|
||||
// Go 会分配一个更大的新数组,将旧数据复制过去,再添加新元素
|
||||
s = append(s, 3) // 容量刚好用完
|
||||
s = append(s, 4) // 触发扩容
|
||||
fmt.Printf("触发扩容后 -> len: %d, cap: %d, ptr: %p\n", len(s), cap(s), s)
|
||||
// 扩容策略:通常是翻倍(当元素较少时)或乘以1.25(当元素较多时)
|
||||
// 输出中可以看到 ptr 地址发生了变化
|
||||
|
||||
// 追加另一个切片(使用 ...)
|
||||
s = append(s, []int{5, 6, 7}...)
|
||||
fmt.Printf("追加切片后 -> len: %d, cap: %d\n", len(s), cap(s))
|
||||
}
|
||||
```
|
||||
|
||||
### 示例 5:复制切片 `copy`
|
||||
|
||||
`copy(dst, src)` 函数用于将 `src` 切片中的元素复制到 `dst` 切片。它只复制**两者长度的最小值**个元素。
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
func main() {
|
||||
src := []int{1, 2, 3, 4, 5}
|
||||
dst := make([]int, 3)
|
||||
|
||||
n := copy(dst, src)
|
||||
|
||||
fmt.Println("复制的元素个数:", n) // 3
|
||||
fmt.Println("目标切片 dst:", dst) // [1 2 3]
|
||||
fmt.Println("源切片 src:", src) // [1 2 3 4 5] - 不受影响
|
||||
|
||||
// copy 创建了独立的副本,修改 dst 不会影响 src
|
||||
dst[0] = 99
|
||||
fmt.Println("修改后 dst:", dst) // [99 2 3]
|
||||
fmt.Println("修改后 src:", src) // [1 2 3 4 5]
|
||||
}
|
||||
```
|
||||
|
||||
> ✅ `copy` 是避免切片共享底层数组副作用的有效手段。
|
||||
|
||||
---
|
||||
|
||||
## 4. 多维切片详解
|
||||
|
||||
多维切片是“切片的切片”,与多维数组不同,其内部的切片长度可以**不一致**。
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
func main() {
|
||||
// 创建一个 "锯齿" 切片
|
||||
matrix := [][]int{
|
||||
{1, 2},
|
||||
{3, 4, 5, 6},
|
||||
{7},
|
||||
}
|
||||
|
||||
fmt.Println("多维切片内容:", matrix)
|
||||
|
||||
matrix[0][1] = 99 // 修改元素
|
||||
fmt.Println("修改后:", matrix)
|
||||
|
||||
// 遍历
|
||||
for i := range matrix {
|
||||
fmt.Println("Row", i, ":", matrix[i])
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 切片作为引用类型的特性
|
||||
|
||||
### 示例 6:赋值和传参共享底层数组
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
func modifySlice(s []string) {
|
||||
s[0] = "MODIFIED"
|
||||
fmt.Println("函数内 s:", s)
|
||||
}
|
||||
|
||||
func main() {
|
||||
original := []string{"a", "b", "c"}
|
||||
fmt.Println("原始切片:", original)
|
||||
|
||||
// 赋值是浅拷贝
|
||||
refCopy := original
|
||||
refCopy[1] = "CHANGED"
|
||||
|
||||
fmt.Println("赋值后 original:", original) // 受影响
|
||||
fmt.Println("赋值后 refCopy:", refCopy)
|
||||
|
||||
// 函数传参也是浅拷贝
|
||||
modifySlice(original)
|
||||
fmt.Println("函数外 original:", original) // 再次受影响
|
||||
}
|
||||
```
|
||||
|
||||
> 输出:
|
||||
> 原始切片: [a b c]
|
||||
> 赋值后 original: [a CHANGED c]
|
||||
> 赋值后 refCopy: [a CHANGED c]
|
||||
> 函数内 s: [MODIFIED CHANGED c]
|
||||
> 函数外 original: [MODIFIED CHANGED c]
|
||||
|
||||
---
|
||||
|
||||
## 6. 切片的常见陷阱与注意事项
|
||||
|
||||
### ❌ 陷阱 1:`append` 可能改变原有切片
|
||||
|
||||
当函数内部的 `append` **没有**触发扩容时,修改会反映到外部;一旦**触发扩容**(返回了新的底层数组),修改就不会反映到外部。
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
func appendTrap(s []int) {
|
||||
// 此时 s 的 len=3, cap=5。append 不会触发扩容
|
||||
s = append(s, 100)
|
||||
fmt.Println("函数内 s:", s) // [0 1 2 100]
|
||||
}
|
||||
|
||||
func main() {
|
||||
original := make([]int, 3, 5) // [0,0,0], len=3, cap=5
|
||||
fmt.Println("调用前 original:", original) // [0 0 0]
|
||||
|
||||
appendTrap(original)
|
||||
|
||||
// original 的长度没变,但底层数组的数据被修改了!
|
||||
fmt.Println("调用后 original:", original) // [0 0 0] - len 没变
|
||||
|
||||
// 查看底层数组的变化
|
||||
fmt.Println("底层数组情况:", original[:cap(original)]) // [0 0 0 100 0]
|
||||
}
|
||||
```
|
||||
|
||||
✅ **正确做法**:函数如果修改了切片(特别是通过 `append`),**永远应该返回新的切片**。
|
||||
|
||||
```go
|
||||
func appendSafe(s []int) []int {
|
||||
return append(s, 100)
|
||||
}
|
||||
|
||||
// 调用: original = appendSafe(original)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### ❌ 陷阱 2:子切片可能导致内存泄漏
|
||||
|
||||
如果你从一个非常大的切片中,只截取一小段并长期持有,那么这个大切片的底层数组将**永远不会被垃圾回收(GC)**。
|
||||
|
||||
```go
|
||||
func getFirstTwo(largeSlice []byte) []byte {
|
||||
// 错误做法:返回的切片与原大切片共享底层数组
|
||||
return largeSlice[:2]
|
||||
// 即使 largeSlice 本身被回收,只要返回的切片还在,整个大数组就在内存中
|
||||
}
|
||||
|
||||
// ✅ 正确做法:使用 copy 创建一个独立的小切片
|
||||
func getFirstTwoSafe(largeSlice []byte) []byte {
|
||||
smallSlice := make([]byte, 2)
|
||||
copy(smallSlice, largeSlice)
|
||||
return smallSlice
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### ❌ 陷阱 3:不能作为 map 的 key
|
||||
|
||||
与可比较的数组不同,切片是不可比较类型(因为它包含指针,且内容可变),因此不能作为 `map` 的键。
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
func main() {
|
||||
// m := make(map[[]int]string) // 编译错误: invalid map key type []int
|
||||
}
|
||||
```
|
||||
|
||||
✅ **替代方案**:可以将切片转换为字符串 `string(slice)` 作为 key,或者使用可比较的数组作为 key。
|
||||
|
||||
---
|
||||
|
||||
## 7. 最佳实践与性能建议
|
||||
|
||||
| 实践 | 说明 |
|
||||
|------|------|
|
||||
| ✅ 优先使用切片 | 除非你需要数组的特性(如 map key),否则总是使用切片。 |
|
||||
| ✅ 预估容量 | 在创建切片时,如果能预估最终大小,使用 `make([]T, 0, capacity)` 来预分配容量,可极大减少 `append` 带来的扩容和复制开销。|
|
||||
| ✅ `append` 后重新赋值 | 任何调用 `append` 的地方,都应将结果赋值回原切片:`s = append(s, …)`。 |
|
||||
| ✅ `copy` 避免副作用 | 当需要一个完全独立的切片,或防止内存泄漏时,使用 `copy`。 |
|
||||
| ✅ 警惕子切片共享数据 | 修改子切片可能会意外改变父切片或其他子切片的数据。 |
|
||||
|
||||
---
|
||||
|
||||
## 8. 典型应用场景
|
||||
|
||||
### 📌 场景 1:动态集合
|
||||
|
||||
任何需要动态增删元素的列表场景,如读取文件行、处理 HTTP 请求参数、解析 JSON 数组等。
|
||||
|
||||
```go
|
||||
// 模拟从数据库读取不定数量的用户 ID
|
||||
var userIDs []int
|
||||
// for rows.Next() {
|
||||
// var id int
|
||||
// ...
|
||||
// userIDs = append(userIDs, id)
|
||||
// }
|
||||
```
|
||||
|
||||
### 📌 场景 2:函数参数和返回值
|
||||
|
||||
作为函数参数传递可变数据集是切片最常见的用途,比传递数组指针更灵活、更安全。
|
||||
|
||||
```go
|
||||
func ProcessItems(items []string) []string {
|
||||
// ... 对 items 进行处理
|
||||
return items
|
||||
}
|
||||
```
|
||||
|
||||
### 📌 场景 3:缓冲区(Buffer)
|
||||
|
||||
在 I/O 操作中,切片被广泛用作读写缓冲区。
|
||||
|
||||
```go
|
||||
// import "os"
|
||||
// f, _ := os.Open("file.txt")
|
||||
// buf := make([]byte, 1024)
|
||||
// n, _ := f.Read(buf)
|
||||
// data := buf[:n] // data 是实际读取到的有效数据
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. 扩容
|
||||
|
||||
### 切片扩容策略
|
||||
|
||||
```
|
||||
1. 计算 newLen = oldLen + 新增数量
|
||||
2. 调用 nextslicecap(newLen, oldCap) → 得到 newcap
|
||||
├─ if newLen > oldCap*2 → 直接返回 newLen
|
||||
├─ if oldCap < 256 → 返回 oldCap * 2
|
||||
└─ else → 循环 newcap += (newcap + 768) / 4,直到 ≥ newLen
|
||||
3. 计算所需内存:capmem = newcap * elemSize
|
||||
4. 调用 roundupsize → 对齐到 mspan size class
|
||||
5. 调整 newcap 以匹配对齐后的内存
|
||||
6. mallocgc 分配新内存
|
||||
7. memmove 复制旧数据 [0, oldLen)
|
||||
8. 返回新切片 {新地址, newLen, newcap}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 扩容机制细节
|
||||
|
||||
#### 1. 是否一定扩容?
|
||||
|
||||
不一定。扩容的条件是 `len == cap`。如果还有剩余容量,`append` 操作只是修改 `len`,不扩容。
|
||||
|
||||
```go
|
||||
s := make([]int, 5, 10) // len=5, cap=10
|
||||
s = append(s, 1, 2, 3) // len=8, cap=10,未扩容
|
||||
```
|
||||
|
||||
#### 2. 扩容一定是新内存吗?
|
||||
|
||||
**是的。** 当触发扩容时,Go 运行时会:
|
||||
|
||||
- 分配一块**全新的、更大的底层数组**
|
||||
- 将原数组数据**复制**到新数组
|
||||
- 返回新的切片(指向新数组)
|
||||
|
||||
原有的切片引用失效,但如果有其他变量引用原底层数组,原数据不会立即被回收(除非无引用)。
|
||||
|
||||
```go
|
||||
a := []int{1, 2, 3}
|
||||
b := a // b 和 a 共享底层数组
|
||||
a = append(a, 4) // a 扩容 → 底层数组复制,a 和 b 不再共享
|
||||
```
|
||||
|
||||
#### 3. 内存对齐与类型影响
|
||||
|
||||
扩容计算还会考虑:
|
||||
|
||||
- 元素大小(如 `int`、`struct{}`)
|
||||
- 内存对齐规则
|
||||
- 分配器策略(如 `runtime.growSlice`)
|
||||
|
||||
因此实际扩容大小可能略大于 `1.25 * cap`,以便按对齐要求分配内存块。
|
||||
|
||||
---
|
||||
|
||||
## 总结
|
||||
|
||||
| 关键点 | 说明 |
|
||||
|--------|------|
|
||||
| 🧩 切片是数组的“视图” | 它是一个包含指针、长度和容量的结构 |
|
||||
| 🔗 引用类型(行为上) | 赋值和传参共享底层数组,修改会相互影响 |
|
||||
| 🚀 动态扩容 | `append` 是核心,当容量不足时会自动扩容(有性能开销)|
|
||||
| ⚠️ 充满陷阱 | 需注意 `append` 的返回值、子切片内存泄漏和共享数据问题 |
|
||||
| 🏆 Go主力数据结构 | 在日常开发中全面取代数组 |
|
||||
|
||||
> ✅ **推荐口诀**:
|
||||
> 切片是数组的窗,指针长度加容量。Append可能要搬家,函数返回新切片。共享内存要当心,拷贝数据保平安。
|
||||
Reference in New Issue
Block a user