Go 语言切片(Slice)笔记
1. 切片概述
切片(Slice)是 Go 语言中一种灵活、动态的数据结构,类似于其他语言中的"可变长数组"。它建立在数组之上,提供了更方便、更安全的序列操作。切片的特点包括:
-
动态长度 :切片的长度(
len)可以在运行时改变。 -
容量管理 :切片有容量(
cap)概念,容量大于等于长度,当长度超过容量时会自动扩容。 -
引用类型:切片本身是一个引用类型,传递时共享底层数组。
-
连续内存:切片元素存储在连续的内存空间中,支持高效的随机访问。
2. 切片数据结构
切片在运行时由一个称为 slice header 的结构体表示,定义在 runtime/slice.go 中:
type slice struct {
array unsafe.Pointer // 指向底层数组的指针
len int // 切片当前长度
cap int // 切片容量
}
-
array:指向底层数组的起始地址。 -
len:切片中实际存储的元素个数,索引范围[0, len)可访问。 -
cap:从array开始到底层数组末尾的总容量,索引范围[0, cap)已分配内存。
3. 创建和初始化切片
3.1 声明但未初始化
var s []int // s == nil,没有分配底层数组
3.2 使用字面量初始化
s := []int{1, 2, 3} // len = 3, cap = 3
3.3 使用 make 创建
s := make([]int, 5) // len = 5, cap = 5,元素初始化为零值
s := make([]int, 5, 10) // len = 5, cap = 10,预留额外空间
-
必须保证
len <= cap,否则编译错误或运行时 panic。 -
当仅指定长度时,容量默认等于长度。
3.4 从数组或切片创建(截取)
arr := [5]int{1,2,3,4,5}
s := arr[1:4] // 从数组创建,s = [2,3,4], len=3, cap=4 (从索引1到数组末尾)
s2 := s[1:] // 从切片创建,s2 = [3,4], len=2, cap=3
截取操作 [low:high] 遵循左闭右开原则,low 和 high 可省略(默认为 0 和 len)。
4. 切片的基本操作
4.1 访问元素
通过索引 s[i] 访问,索引范围 0 <= i < len(s),越界会导致 panic。
4.2 截取切片
s := []int{0,1,2,3,4,5}
s1 := s[2:5] // [2,3,4], len=3, cap=4 (从索引2到底层数组末尾)
s2 := s[:3] // [0,1,2], len=3, cap=6
s3 := s[3:] // [3,4,5], len=3, cap=3
s4 := s[:] // 整个切片,len=6, cap=6
截取产生的新切片与原始切片共享底层数组,修改会影响对方。
4.3 追加元素(append)
s := []int{1,2,3}
s = append(s, 4) // [1,2,3,4], len=4, cap 可能变化
s = append(s, 5,6) // 追加多个
s = append(s, []int{7,8}...) // 追加另一个切片(需要展开)
-
append在尾部追加元素,如果len < cap,直接使用现有空间;否则触发扩容。 -
必须将返回值赋值给原切片(因为可能扩容导致底层数组改变)。
4.4 删除元素
Go 没有内置删除函数,通常使用截取和追加组合实现。
-
删除开头元素:
s = s[1:] // 删除第一个 s = s[n:] // 删除前n个 -
删除末尾元素:
s = s[:len(s)-1] // 删除最后一个 s = s[:len(s)-n] // 删除后n个 -
删除中间元素:
i := 2 // 要删除的索引 s = append(s[:i], s[i+1:]...) -
清空切片(保留容量):
s = s[:0] // len=0, cap不变
4.5 拷贝切片(copy)
src := []int{1,2,3}
dst := make([]int, len(src))
n := copy(dst, src) // n = 3,dst 独立于 src
-
copy返回实际复制的元素个数(取min(len(dst), len(src)))。 -
dst和src可以重叠,复制正确。 -
浅拷贝:只复制元素值,不复制元素内部的引用(但如果元素是指针,则共享指向的对象)。
4.6 遍历切片
for i, v := range s {
// i 是索引,v 是元素值的副本
}
for i := range s {
// 只使用索引
}
for _, v := range s {
// 只使用值
}
5. 切片的引用传递特性
切片本身是一个包含指针的小结构体(slice header),在函数间传递时,会复制 slice header(值传递),但复制的 header 中的 array 指针指向同一个底层数组。因此,对切片元素的修改会反映到原切片;但对切片长度、容量的修改(如 append 导致扩容)不会影响原切片,除非返回新切片并重新赋值。
func modify(s []int) {
s[0] = 100 // 修改元素,原切片受影响
s = append(s, 200) // 可能扩容,s 变成新 header,原切片不受影响
}
6. 切片的扩容机制
当 append 导致长度超过容量时,切片会扩容。扩容规则(Go 1.19+):
-
如果新容量 > 旧容量的两倍,直接使用新容量。
-
否则,如果旧容量 < 256,新容量 = 旧容量 * 2。
-
否则,循环计算新容量:
newcap = oldcap + (oldcap + 3*256)/4,直到newcap >= 所需容量。 -
扩容后,内存分配会按
mallocgc的对齐规则进行向上取整,可能导致最终容量略大于计算值(受内存分配类影响)。
示例:旧容量 512,追加 1 个元素后,计算新容量:
-
所需容量 = 513,旧容量 512 ≥ 256,计算:
- 512 + (512+768)/4 = 512 + 1280/4 = 512 + 320 = 832,满足 ≥513,新容量 = 832。
-
内存对齐:元素大小 8 字节,832*8 = 6656 字节,实际分配的内存块可能为 6784 字节(对应 size class 49),因此最终容量 = 6784/8 = 848。
扩容过程:
-
分配新底层数组(可能更大)。
-
将旧元素拷贝到新数组。
-
返回新切片(
array指向新地址,len增加,cap更新)。
7. 切片的注意事项和常见陷阱
7.1 长度与容量的区别
-
len是可访问元素的数量,cap是已分配内存能容纳的最大元素数。 -
访问
s[len]到s[cap-1]会导致 panic,即使内存已分配。 -
截取操作可能产生容量很大的切片,隐藏了大量未使用的内存。
7.2 切片共享底层数组的副作用
截取和简单赋值产生的切片共享底层数组,修改一个会影响其他。当需要独立副本时,应使用 copy 或完整复制。
7.3 扩容导致地址变更
append 可能触发扩容,扩容后底层数组改变,旧切片与新切片不再共享内存。如果多个变量引用同一底层数组,扩容后需注意更新所有引用。
7.4 内存泄漏风险
截取大切片的一部分并持有,可能导致大数组无法被垃圾回收。例如:
var all []byte // 一个大切片
part := all[10:20] // part 持有整个底层数组,即使只引用小部分
解决方案:使用 copy 将所需部分复制到新切片。
7.5 切片不是并发安全的
多个 goroutine 并发读写同一切片(包括 append)需要加锁或使用其他同步机制。
7.6 零值切片与空切片
-
var s []int是nil切片,没有底层数组,len和cap都是 0。 -
s := []int{}或make([]int, 0)是非 nil 的空切片,有底层数组(但长度为 0)。 -
两者在
append时行为一致,但在 JSON 序列化等场景有区别(nil序列化为null,空切片序列化为[])。
7.7 append 导致容量变化的不确定性
由于扩容规则和内存对齐,不能依赖 cap 的精确值,只应关注是否满足需求。
8. 问题解答汇总
问题1
s := make([]int, 10) // len=10, cap=10
s = append(s, 10) // len=11, cap=20 (原容量<256,翻倍)
答案 :[0 0 0 0 0 0 0 0 0 0 10], len=11, cap=20
问题2
s := make([]int, 0, 10) // len=0, cap=10
s = append(s, 10) // len=1, cap=10 (容量足够,不扩容)
答案 :[10], len=1, cap=10
问题3
s := make([]int, 10, 11) // len=10, cap=11
s = append(s, 10) // len=11, cap=11 (容量足够)
答案 :[0 0 0 0 0 0 0 0 0 0 10], len=11, cap=11
问题4
s := make([]int, 10, 12) // len=10, cap=12
s1 := s[8:] // 从索引8开始,len=2, cap=4
答案 :[0 0], len=2, cap=4
问题5
s := make([]int, 10, 12)
s1 := s[8:9] // len=1, cap=4 (容量仍从索引8到原数组末尾)
答案 :[0], len=1, cap=4
问题6
s := make([]int, 10, 12)
s1 := s[8:]
s1[0] = -1 // 修改共享元素
答案 :s 变为 [0 0 0 0 0 0 0 0 -1 0]
问题7
s := make([]int, 10, 12)
v := s[10] // 索引10 ≥ len(10),越界
答案:发生 panic
问题8
s := make([]int, 10, 12)
s1 := s[8:]
s1 = append(s1, []int{10,11,12}...) // s1 扩容,底层数组改变
v := s[10] // s 仍为原数组,len=10,s[10] 越界
答案 :访问 s[10] 发生 panic
问题9
s := make([]int, 10, 12)
s1 := s[8:]
changeSlice(s1) // 修改 s1[0] = -1
func changeSlice(s1 []int) { s1[0] = -1 }
答案 :s 变为 [0 0 0 0 0 0 0 0 -1 0]
问题10
s := make([]int, 10, 12)
s1 := s[8:]
changeSlice(s1) // 内部 append,不影响外部 s1 的 len/cap
func changeSlice(s1 []int) { s1 = append(s1, 10) }
答案:
-
s:[0 0 0 0 0 0 0 0 0 0], len=10, cap=12 -
s1:[0 0], len=2, cap=4 (外部 s1 未变)
问题11
s := []int{0,1,2,3,4}
s = append(s[:2], s[3:]...) // 删除索引2的元素
答案 :s 变为 [0 1 3 4], len=4, cap=5,访问 s[4] 越界 panic
问题12
s := make([]int, 512) // len=512, cap=512
s = append(s, 1) // 触发扩容
答案:len=513, cap=848 (扩容规则+内存对齐)
9. 总结
-
切片是 Go 中灵活、高效的序列类型,理解其内部结构(slice header)是正确使用的基础。
-
注意区分长度和容量,避免越界。
-
共享底层数组的特性容易导致意外副作用,需谨慎处理截取和传递。
-
扩容机制随 Go 版本变化,了解当前版本的规则有助于容量预估和性能优化。
-
切片非并发安全,多 goroutine 操作需加锁。
-
使用
copy创建独立副本,避免内存泄漏。
掌握切片的底层原理和常见陷阱,能帮助开发者写出更健壮、高效的 Go 代码。