一.切片
如果说数组是固定大小的储物柜,那么切片就像是一个可以随时扩展的"魔法储物柜"。它的大小是可以动态调整的,这让我们在处理不确定数量的数据时更加灵活。
slice 本质是什么?(核心原理)
在 Go 里,slice 并不存数据,它只是一个结构体:
type slice struct {
ptr *T // 指向底层数组的某个位置
len int // 当前能用的长度
cap int // 从 ptr 开始,到数组末尾还能用多少
}
记住一句话:
cap 不是“我现在有多少”,而是“我最多还能用多少”
切片的创建方式
// 1. 直接创建
fruits := []string{"苹果", "香蕉", "橙子"}
fmt.Printf("fruits:%v\n", fruits)
// 2. 使用make函数创建
numbers := make([]int, 3, 5) // 长度为3,容量为5的切片
fmt.Printf("numbers:%v\n", numbers)
// 3. 从数组创建
arr := [5]int{1, 2, 3, 4, 5}
slice := arr[1:4] // [2 3 4]
fmt.Printf("slice:%v\n", slice)
切片的三个核心概念
- 指针:指向底层数组的第一个可见元素
- 长度:切片当前的元素个数(len)
- 容量:从切片起始位置到底层数组末尾的元素个数(cap)
- 如果切片还没有用完容量,则不会分配新数组,使用的还是原数组
实用的切片操作
//创建一个购物清单
shoppingList := []string{"面包"}
//新增商品
shoppingList = append(shoppingList, "油条")
shoppingList = append(shoppingList, "牛奶", "玉米")
fmt.Printf("购物清单:%v\n", shoppingList)
fmt.Printf("清单长度:%d\n", len(shoppingList))
fmt.Printf("清单容量:%d\n", cap(shoppingList))
//复制切片
backup := make([]string, len(shoppingList))
copy(backup, shoppingList)
fmt.Printf("备份清单:%v\n", backup)
输出:
购物清单:[面包 油条 牛奶 玉米]
清单长度:4
清单容量:4
备份清单:[面包 油条 牛奶 玉米]
切片的注意事项
- 切片是引用类型,多个切片可能共享同一个底层数组
- append可能导致重新分配内存,生成新的底层数组
- 使用make创建切片时,可以指定容量来减少内存重新分配的次数
二.实际应用场景对比
- 什么时候用数组?
-
- 当你确切知道数据的长度,且不会改变时
- 例如:存储一周七天的温度数据
- 什么时候用切片?
-
- 处理动态数据,如用户输入的列表
- 需要对数据进行频繁的添加和删除操作
- 作为函数参数传递(更加灵活)
三.切片的扩容机制
理解切片的扩容机制就像了解一个智能存储系统的工作原理,这对写出高效的 Go 程序至关重要。
小容量时翻倍,大容量时缓慢增长
具体规则 :
|
当前 cap |
新 cap |
|
|
|
|
|
(逐渐变慢) |
示例:
s := make([]int, 0)
lastCap := cap(s)
for i := 0; i < 50; i++ {
s = append(s, i)
if cap(s) != lastCap {
fmt.Printf("len=%d cap=%d\n", len(s), cap(s))
lastCap = cap(s)
}
}
输出:
len=1 cap=1
len=2 cap=2
len=3 cap=4
len=5 cap=8
len=9 cap=16
len=17 cap=32
可以看出 是 典型的 2 倍增长
如果是大于1024后 将是1.25倍增长
示例:
s := make([]int, 0)
lastCap := cap(s)
fmt.Printf("初始容量:%d\n", lastCap)
for i := 0; i < 5000; i++ {
s = append(s, i)
if cap(s) != lastCap {
fmt.Printf("len=%d cap=%d\n", len(s), cap(s))
lastCap = cap(s)
}
}
输出:
初始容量:0
len=1 cap=1
len=2 cap=2
len=3 cap=4
len=5 cap=8
len=9 cap=16
len=17 cap=32
len=33 cap=64
len=65 cap=128
len=129 cap=256
len=257 cap=512
len=513 cap=848
len=849 cap=1280
len=1281 cap=1792
len=1793 cap=2560
len=2561 cap=3408
len=3409 cap=5120
1.为什么 append 会“污染”别的 slice?(本质原因)
a := []int{1, 2, 3, 4}
b := a[:2] // len=2 cap=4
b = append(b, 99)
因为:
b.cap = 4- append 没超 cap
- 直接写到了
a[2]
结果:
a = [1 2 99 4]
⚠️ 这是 slice 共享底层数组的必然后果
2. 如何 100% 控制 append 是否扩容
✅ 方法 1:提前 make 好 cap(最常用)
s := make([]int, 0, 1000)
👉 高性能,避免多次扩容
✅ 方法 2:三参数切片(防止污染)
b := a[:2:2] // len=2 cap=2
b = append(b, 99) // 一定扩容
面试级总结
- append 是否扩容只看:
len + n > cap - 不扩容:共享底层数组
- 扩容:新数组 + 拷贝
- 小 cap 翻倍,大 cap 缓慢增长
- slice 之间的“诡异互相影响”,100% 来自共享底层数组
四.copy() 和 append() 有什么本质区别
一句话总纲(先背):
append 是“扩展 slice”
copy 是“拷贝数据”
更直白一点:
append:我要往 slice 里加东西copy:我要一个和你内容一样,但互不影响的 slice
一、最本质的区别(核心)
|
对比点 |
append |
copy |
|
是否创建新元素 |
✅ |
❌ |
|
是否可能分配新数组 |
⚠️ 可能 |
❌(只写已有空间) |
|
是否解决共享底层数组 |
❌ |
✅ |
|
是否改变 len |
✅ |
❌ |
|
是否返回新 slice |
✅ |
❌(返回拷贝个数) |
一句话总结:
append 管“长度”
copy 管“内存独立性”
二、append 的本质
b := append(a, x)
背后逻辑:
- 看 cap 够不够
- 够 → 直接写底层数组
- 不够 → 新数组 + copy 旧数据 + 追加新数据
⚠️ 注意:
无法控制 append 一定扩容(除非三参数切片)
三、copy 的本质
n := copy(dst, src)
copy 的前提条件非常重要:
dst 必须已经有长度(len > 0)
dst := []int{} // len = 0
copy(dst, src) // 拷贝 0 个 ❌
正确姿势:
dst := make([]int, len(src))
copy(dst, src)
四、什么时候“必须”用 copy?(重点)
场景 1️⃣:函数里保存 slice(最常见)
func save(s []byte) {
global = s // ❌ 危险
}
如果外部还会改 s,就出问题了。
✅ 正确做法:
func save(s []byte) {
//创建一个切片,将切片s复制到新切片中
buf := make([]byte, len(s))
copy(buf, s)
global = buf
}
📌 规则:
只要 slice 要“活得比调用者久”,就必须 copy
场景 2️⃣:从 buffer / pool / io 读数据
buf := make([]byte, 1024)
n, _ := conn.Read(buf)
process(buf[:n]) // ❌
buf 很可能会被复用。
✅ 正确:
data := make([]byte, n)
copy(data, buf[:n])
process(data)
这是 Go 网络编程铁律
场景 3️⃣:从大 slice 切小 slice(内存泄漏)
big := make([]byte, 10<<20) // 10MB
small := big[:100]
👉 small一直引用 10MB
✅ 正确:
small := make([]byte, 100)
copy(small, big[:100])
五.什么时候绝对不要用 copy?
❌ 情况:你只是想加元素
s := []int{1, 2}
copy(s, []int{3}) // ❌ 错误理解
copy 不会扩容、不会 append
六.一个对比示例(非常直观)
a := []int{1, 2, 3}
b := a[:2]
c := make([]int, 3)
copy(c, a[:2])
fmt.Println(a)
fmt.Println(b)
fmt.Println(c)
b[0] = 100 //由于是共享数组 会改变a的值
c[1] = 200 //copy后,是独立的 不会改变a的值
fmt.Println(a)
fmt.Println(b)
fmt.Println(c)
👉 b 共享
👉 c 独立
输出:
[1 2 3]
[1 2]
[1 2 0]
[100 2 3]
[100 2]
[1 200 0
七. 工程级判断口诀
看到 slice → 问三件事:
1️⃣ 我会不会 append?
2️⃣ 会不会跨函数 / goroutine 使用?
3️⃣ 原 slice 会不会再被改?
只要有一个是“是” → 用 copy
五.预分配优化策略
func SliceDemo11() {
fmt.Println("=== 算法导航性能优化实验 ===")
const dataSize = 10000000
// 测试1:不预分配容量
start := time.Now()
var slice1 []int
for i := 0; i < dataSize; i++ {
slice1 = append(slice1, i)
}
duration1 := time.Since(start)
// 测试2:预分配足够容量
start = time.Now()
slice2 := make([]int, 0, dataSize)
for i := 0; i < dataSize; i++ {
slice2 = append(slice2, i)
}
duration2 := time.Since(start)
// 测试3:预分配长度并直接赋值
start = time.Now()
slice3 := make([]int, dataSize)
for i := 0; i < dataSize; i++ {
slice3[i] = i
}
duration3 := time.Since(start)
fmt.Printf("处理 %d 个元素的性能对比:\n", dataSize)
fmt.Printf("不预分配: %v\n", duration1)
fmt.Printf("预分配容量: %v (提升 %.2fx)\n", duration2,
float64(duration1)/float64(duration2))
fmt.Printf("预分配长度: %v (提升 %.2fx)\n", duration3,
float64(duration1)/float64(duration3))
// 实际应用建议
fmt.Println("\n=== 实际应用建议 ===")
// 示例:编程导航用户数据处理
expectedUserCount := 10000
// 好的做法:预分配容量
users := make([]map[string]interface{}, 0, expectedUserCount)
fmt.Printf("预分配用户切片容量: %d\n", cap(users))
// 模拟添加用户数据
for i := 0; i < 5; i++ {
user := map[string]interface{}{
"id": i + 1,
"name": fmt.Sprintf("user%d", i+1),
"level": i%3 + 1,
}
users = append(users, user)
}
fmt.Printf("添加5个用户后: 长度=%d, 容量=%d\n", len(users), cap(users))
fmt.Println("✅ 没有发生扩容,性能最优")
}
输出:
=== 算法导航性能优化实验 ===
处理 10000000 个元素的性能对比:
不预分配: 116.2406ms
预分配容量: 17.4931ms (提升 6.64x)
预分配长度: 19.9891ms (提升 5.82x)
=== 实际应用建议 ===
预分配用户切片容量: 10000
添加5个用户后: 长度=5, 容量=10000
✅ 没有发生扩容,性能最
从上面的输出可得知,预分配的性能比不预分配性能好很多
4万+

被折叠的 条评论
为什么被折叠?



