6.1 Go 切片学习笔记
1. 切片基础 --- 创建与共享底层数组
切片是 Go 中最核心的数据结构,它是动态数组的"视图",由指针、长度、容量三部分组成:
go
package main
import "fmt"
func main() {
// 从数组创建切片:arr[low:high],包含 low 到 high-1 的元素
arr := [5]int{1, 2, 3, 4, 5}
slice1 := arr[1:4] // [2, 3, 4]
// make() 创建切片:make([]type, length, capacity)
slice2 := make([]string, 3, 5) // 长度3,容量5,元素为string零值""
// 长度3:已有3个""元素可访问;容量5:底层数组可容纳5个元素
// 切片字面量:直接声明(与数组字面量不同,没有长度)
slice3 := []int{10, 20, 30} // 切片,不是数组!没有 [N]
// nil 切片:var 声明未初始化
var slice4 []int // slice4 == nil,len=0, cap=0
if slice4 == nil {
fmt.Println("slice4 is nil")
}
// len() 和 cap() 获取长度和容量
length := len(slice2)
capacity := cap(slice2)
// 修改切片元素会影响原数组(共享底层内存)
slice1[1] = 99 // arr[2] 也变成 99
fmt.Println("Original array:", arr)
fmt.Println("slice1:", slice1)
fmt.Println("slice2:", slice2)
fmt.Println("slice3:", slice3)
fmt.Printf("slice2 - length: %d, capacity: %d\n", length, capacity)
}
执行结果:
slice4 is nil
Original array: [1 2 99 4 5]
slice1: [2 99 4]
slice2: [ ]
slice3: [10 20 30]
slice2 - length: 3, capacity: 5
要点:
- 切片 ≠ 数组:数组有固定长度
[5]int,切片没有固定长度[]int arr[1:4]--- 从数组创建切片,包含索引 1 到 3 的元素(不含索引4)slice1[1] = 99修改了切片元素,原数组arr[2]也变成 99 --- 切片和数组共享底层内存make([]string, 3, 5)--- 创建长度3、容量5的切片,前3个元素为零值 "",还有2个预留空间[]int{10, 20, 30}--- 切片字面量(没有长度数字),Go 自动创建底层数组var slice4 []int--- nil 切片,可以== nil判断(与空切片[]int{}不同,空切片 != nil)- slice2 输出
[ ]--- 3个空字符串(string 的零值是 "")
2. append --- 追加元素与容量增长
append() 向切片追加元素,超过容量时自动扩容:
go
package main
import "fmt"
func main() {
// 从 nil 切片开始 append(完全合法)
var numbers []int
numbers = append(numbers, 1) // 追加单个元素
numbers = append(numbers, 2, 3, 4) // 追加多个元素
// 用 ... 展开切片追加
moreNumbers := []int{5, 6, 7}
numbers = append(numbers, moreNumbers...) // 展开追加
// 观察容量增长:make([]string, 0, 2)
fruits := make([]string, 0, 2) // 长度0,容量2
fmt.Printf("Initial - len: %d, cap: %d\n", len(fruits), cap(fruits))
fruits = append(fruits, "apple")
fmt.Printf("After 1 append - len: %d, cap: %d\n", len(fruits), cap(fruits))
fruits = append(fruits, "banana")
fmt.Printf("After 2 appends - len: %d, cap: %d\n", len(fruits), cap(fruits))
// 第3次 append 超出容量,触发扩容:2 → 4
fruits = append(fruits, "cherry")
fmt.Printf("After 3 appends - len: %d, cap: %d\n", len(fruits), cap(fruits))
// nil 切片可以直接 append
var colors []string // nil 切片
colors = append(colors, "red") // append 自动分配底层数组
fmt.Println("Numbers:", numbers)
fmt.Println("Fruits:", fruits)
fmt.Println("Colors:", colors)
}
执行结果:
Initial - len: 0, cap: 2
After 1 append - len: 1, cap: 2
After 2 appends - len: 2, cap: 2
After 3 appends - len: 3, cap: 4
Numbers: [1 2 3 4 5 6 7]
Fruits: [apple banana cherry]
Colors: [red]
要点:
-
append(slice, element)--- 返回新切片(长度+1),必须用=接住返回值 :gonumbers = append(numbers, 1) // 必须赋值!append 可能返回新底层数组 -
append(numbers, 2, 3, 4)--- 同时追加多个元素 -
append(numbers, moreNumbers...)--- 用...展开切片追加 -
容量增长过程:
- Initial: len=0, cap=2(预留2个位置)
- append "apple": len=1, cap=2(还剩1个位置)
- append "banana": len=2, cap=2(满了)
- append "cherry": len=3, cap=4(超出容量,扩容到4)--- 容量翻倍!
-
nil 切片可以 append:Go 自动分配底层数组,无需 make 初始化
-
扩容策略:通常翻倍(2→4→8→16...),但 Go 运行时可能根据情况调整
3. 浅拷贝与深拷贝 --- copy()
切片的"拷贝"有浅拷贝(共享底层数组)和深拷贝(独立底层数组)两种:
go
package main
import "fmt"
func main() {
original := []int{1, 2, 3, 4, 5}
// 浅拷贝:original[:] 创建新切片头,但共享底层数组
shallow := original[:]
shallow[0] = 999 // 修改 shallow 也修改 original!
fmt.Println("After modifying shallow copy:")
fmt.Println("Original:", original) // [999 2 3 4 5] --- 被修改了
fmt.Println("Shallow:", shallow) // [999 2 3 4 5]
// 重置 original
original[0] = 1
// 深拷贝:copy() 创建独立底层数组
deep := make([]int, len(original)) // 先创建目标切片
copy(deep, original) // copy(dest, src) --- 复制元素
deep[0] = 888 // 修改 deep 不影响 original
fmt.Println("\nAfter modifying deep copy:")
fmt.Println("Original:", original) // [1 2 3 4 5] --- 未被修改
fmt.Println("Deep:", deep) // [888 2 3 4 5]
// copy 返回复制的元素数量
source := []string{"a", "b", "c", "d", "e"}
destination := make([]string, 3) // 目标长度3
copied := copy(destination, source) // 只能复制3个(目标容量限制)
fmt.Printf("\nCopied %d elements: %v\n", copied, destination)
// 小切片复制到大切片
small := []int{100, 200}
large := make([]int, 5)
n := copy(large, small) // 只复制2个,剩余3个保持零值0
fmt.Printf("Copied %d elements to large slice: %v\n", n, large)
}
执行结果:
After modifying shallow copy:
Original: [999 2 3 4 5]
Shallow: [999 2 3 4 5]
After modifying deep copy:
Original: [1 2 3 4 5]
Deep: [888 2 3 4 5]
Copied 3 elements: [a b c]
Copied 2 elements to large slice: [100 200 0 0 0]
要点:
- 浅拷贝
original[:]--- 新切片头 + 共享底层数组,修改互相影响 - 深拷贝
copy(dest, src)--- 创建独立底层数组,修改互不影响- 步骤:先
make([]int, len)创建目标 → 再copy(dest, src)复制元素
- 步骤:先
copy(dest, src)返回实际复制的元素数 = min(len(dest), len(src))- 目标长度3,源长度5:只复制3个 →
[a b c] - 源长度2,目标长度5:复制2个,剩余保持零值 →
[100 200 0 0 0]
- 目标长度3,源长度5:只复制3个 →
copy不会扩容目标切片!目标切片必须预先make并有足够长度
4. 切片容量与底层共享 --- 深入理解
切片容量是从切片起始位置到底层数组末尾的元素数量,理解容量是理解切片的关键:
go
package main
import "fmt"
func main() {
// make 指定长度和容量
slice1 := make([]int, 5, 10) // 5个零值元素,底层数组容量10
fmt.Printf("slice1 - len: %d, cap: %d, slice: %v\n", len(slice1), cap(slice1), slice1)
for i := range slice1 {
slice1[i] = i * 2
}
fmt.Printf("After filling: %v\n", slice1)
// 从数组切片:容量 = 底层数组从起始位置到末尾的长度
array := [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
slice2 := array[2:6] // 索引2到5,长度4
// 容量 = 10 - 2 = 8(从索引2到数组末尾)
fmt.Printf("slice2 - len: %d, cap: %d, slice: %v\n", len(slice2), cap(slice2), slice2)
// 修改切片会影响原数组
slice2[0] = 999 // array[2] 也变成 999
fmt.Printf("Array after modifying slice2: %v\n", array)
fmt.Printf("slice2 after modification: %v\n", slice2)
// append 超出容量时触发扩容
smallSlice := make([]int, 2, 3) // 长度2,容量3
fmt.Printf("Initial smallSlice - len: %d, cap: %d\n", len(smallSlice), cap(smallSlice))
smallSlice[0] = 10
smallSlice[1] = 20
fmt.Println("Appending elements and observing capacity:")
// 第1次 append:还在容量内
smallSlice = append(smallSlice, 30)
fmt.Printf("After append(30) - len: %d, cap: %d, slice: %v\n", len(smallSlice), cap(smallSlice), smallSlice)
// 第2次 append:超出容量,扩容 3→6
smallSlice = append(smallSlice, 40)
fmt.Printf("After append(40) - len: %d, cap: %d, slice: %v\n", len(smallSlice), cap(smallSlice), smallSlice)
// 第3次 append 多个元素:7个 > 容量6,扩容 6→12
smallSlice = append(smallSlice, 50, 60, 70)
fmt.Printf("After append(50,60,70) - len: %d, cap: %d, slice: %v\n", len(smallSlice), cap(smallSlice), smallSlice)
}
执行结果:
slice1 - len: 5, cap: 10, slice: [0 0 0 0 0]
After filling: [0 2 4 6 8]
slice2 - len: 4, cap: 8, slice: [3 4 5 6]
Array after modifying slice2: [1 2 999 4 5 6 7 8 9 10]
slice2 after modification: [999 4 5 6]
Initial smallSlice - len: 2, cap: 3
Appending elements and observing capacity:
After append(30) - len: 3, cap: 3, slice: [10 20 30]
After append(40) - len: 4, cap: 6, slice: [10 20 30 40]
After append(50,60,70) - len: 7, cap: 12, slice: [10 20 30 40 50 60 70]
要点:
make([]int, 5, 10)--- 长度5(可访问5个元素),容量10(底层数组有10个位置)array[2:6]的容量 = 10-2 = 8 --- 从索引2到数组末尾(索引0-9),还有8个位置可用slice2[0] = 999→array[2] = 999--- 切片与数组共享内存- append 扩容过程:
- append(30):len 2→3, cap 3(刚好满了)
- append(40):len 3→4, cap 3→6(超出容量,分配新底层数组,容量翻倍)
- append(50,60,70):len 4→7, cap 6→12(再次超出,容量翻倍)
- 关键:扩容时分配新底层数组,切片不再与原数组共享内存!
5. 切片重叠与完整切片表达式 [low:high:max]
多个切片可以共享同一底层数组,修改重叠区域会互相影响;完整切片表达式可以限制容量:
go
package main
import "fmt"
func main() {
base := []int{1, 2, 3, 4, 5, 6, 7, 8}
slice3 := base[1:4] // [2, 3, 4],容量 = 8-1 = 7
slice4 := base[3:6] // [4, 5, 6],容量 = 8-3 = 5
fmt.Printf("base: %v\n", base)
fmt.Printf("slice3 [1:4]: %v (len: %d, cap: %d)\n", slice3, len(slice3), cap(slice3))
fmt.Printf("slice4 [3:6]: %v (len: %d, cap: %d)\n", slice4, len(slice4), cap(slice4))
// slice3[2] 对应 base[3],slice4[0] 也对应 base[3] --- 重叠!
slice3[2] = 999 // base[3] 变成 999,slice4[0] 也变成 999
fmt.Printf("After modifying slice3[2] to 999:\n")
fmt.Printf("base: %v\n", base) // [1 2 3 999 5 6 7 8]
fmt.Printf("slice3: %v\n", slice3) // [2 3 999]
fmt.Printf("slice4: %v\n", slice4) // [999 5 6] --- 被影响了!
// 完整切片表达式:[low:high:max],max 限制容量
fullSlice := base[1:4:5] // len = 4-1 = 3, cap = 5-1 = 4
fmt.Printf("Full slice expression [1:4:5] - len: %d, cap: %d, slice: %v\n",
len(fullSlice), cap(fullSlice), fullSlice)
}
执行结果:
base: [1 2 3 4 5 6 7 8]
slice3 [1:4]: [2 3 4] (len: 3, cap: 7)
slice4 [3:6]: [4 5 6] (len: 3, cap: 5)
After modifying slice3[2] to 999:
base: [1 2 3 999 5 6 7 8]
slice3: [2 3 999]
slice4: [999 5 6]
Full slice expression [1:4:5] - len: 3, cap: 4, slice: [2 3 999]
要点:
base[1:4]容量 = 7(从 base 索引1到末尾,8-1=7)base[3:6]容量 = 5(从 base 索引3到末尾,8-3=5)- slice32 和 slice40 都指向 base3 --- 重叠区域,修改一个另一个也变
base[1:4:5]--- 完整切片表达式(3索引),max=5限制容量 = 5-1 = 4- 没有第三个索引时,容量到底层数组末尾(7)
- 有第三个索引时,容量被限制为 max-low(4),更安全
- 好处:append 不会侵入其他切片的区域,超出限制容量时会分配新底层数组
6. 扩容策略观察 --- 容量增长规律
观察 append 时容量的实际增长:
go
package main
import "fmt"
func main() {
fmt.Println("Demonstrating memory reallocation:")
growSlice := []int{1} // len=1, cap=1
for i := 0; i < 5; i++ {
fmt.Printf("Before append - len: %d, cap: %d\n", len(growSlice), cap(growSlice))
growSlice = append(growSlice, i)
fmt.Printf("After append - len: %d, cap: %d\n", len(growSlice), cap(growSlice))
fmt.Println()
}
}
执行结果:
Demonstrating memory reallocation:
Before append - len: 1, cap: 1
After append - len: 2, cap: 2
Before append - len: 2, cap: 2
After append - len: 3, cap: 4
Before append - len: 3, cap: 4
After append - len: 4, cap: 4
Before append - len: 4, cap: 4
After append - len: 5, cap: 8
Before append - len: 5, cap: 8
After append - len: 6, cap: 8
要点:
- 扩容规律:1 → 2 → 4 → 8 → ...(大致翻倍)
- 容量4时 append 1个元素:len 从3→4,不需要扩容(还在容量内)
- 容量4时再 append:len 从4→5,超出容量,扩容到8
- 详细过程:
- cap=1, append: len 1→2, cap 1→2(翻倍)
- cap=2, append: len 2→3, cap 2→4(翻倍)
- cap=4, append: len 3→4, cap 不变(容量够用)
- cap=4, append: len 4→5, cap 4→8(翻倍)
- cap=8, append: len 5→6, cap 不变(容量够用)
- 扩容时 Go 分配新底层数组,拷贝旧数据到新数组,旧数组由 GC 回收
- 性能提示:如果知道大致需要多少元素,用
make([]T, 0, n)预分配容量,避免反复扩容
知识点总结
| 知识点 | 关键概念 |
|---|---|
| 切片创建 | 数组切片 arr[1:4]、make([]T, len, cap)、字面量 []int{...}、nil 切片 |
| 共享底层数组 | 切片修改会影响原数组,这是切片的核心特性(也是常见 bug 来源) |
| append 追加 | 返回新切片,必须接住;超出容量自动扩容;nil 切片可以 append |
| 浅拷贝 vs 深拷贝 | [:] 共享底层 vs copy() 独立底层 |
| copy 函数 | copy(dest, src) 返回复制数量 = min(len(dest), len(src)) |
| 容量计算 | arr[low:high] 的 cap = len(array) - low |
| 重叠切片 | 同底层数组的多个切片有重叠区域,修改互相影响 |
| 完整切片表达式 | [low:high:max] 限制容量,防止 append 侵入其他切片 |
| 扩容策略 | 大致翻倍(1→2→4→8...),预分配容量可避免频繁扩容 |