12 - Go Slice:底层原理、扩容机制与常见坑位
文章目录
- 12 - Go Slice:底层原理、扩容机制与常见坑位(超详细)
- 什么是 Slice?
- Slice 和数组的区别
- Slice 的底层结构(核心重点)
- Slice 的创建方式
- 基于数组
- 使用 make
- 直接初始化
- Slice 的核心操作
- 📌 append(重点)
- 📌 copy
- 📌 截取(共享底层数组)
- Slice 扩容机制(面试高频🔥)
- 📌 规则(Go 1.18+)
- 📌 示例
- 📌 扩容本质
- Slice 常见坑(非常重要🔥)
- ❗坑 共享底层数组导致数据污染
- ❗坑 函数传参修改问题
- Slice 最佳实践
- 提前分配容量
- 避免共享数据污染
- 使用 copy 做深拷贝
- 面试高频问题总结
- 🔥 Q:Slice 是值类型还是引用类型?
- 🔥 Q:append 一定会扩容吗?
- 🔥 Q:slice 扩容后原数据还在吗?
- 🔥 Q:为什么修改 slice 会影响原数组?
- 一句话总结
- 🚀 结语
12 - Go Slice:底层原理、扩容机制与常见坑位(超详细)
在 Go 语言中,slice(切片)是最常用的数据结构之一。很多人会用,但不一定真的理解它。
这篇文章带你从本质 → 原理 → 实战 → 踩坑 → 面试全面掌握 slice。
什么是 Slice?
📌 本质一句话:
Slice 是对数组的一个“动态视图”(引用类型)
Slice 和数组的区别
| 对比项 | 数组 | Slice |
|---|---|---|
| 长度 | 固定 | 可变 |
| 类型 | 值类型 | 引用类型 |
| 传参 | 值拷贝 | 引用传递 |
| 灵活性 | 低 | 高 |
Slice 的底层结构(核心重点)
Slice 并不是一个简单的数据结构,它底层是一个结构体:
typeslicestruct{array unsafe.Pointer// 指向底层数组lenint// 当前长度capint// 容量}📌 图解理解
slice ↓ +---------------------+ | ptr | len | cap | +---------------------+ ↓ 底层数组 [1 2 3 4 5]Slice 的创建方式
基于数组
packagemainimport"fmt"funcmain(){arr:=[5]int{1,2,3,4,5}s:=arr[1:4]fmt.Println(s)// 输出:[2 3 4]fmt.Println(len(s))// 输出:3fmt.Println(cap(s))// 输出:4}使用 make
packagemainimport"fmt"funcmain(){s:=make([]int,3,5)fmt.Println(s)// 输出:[0 0 0]fmt.Println(len(s))// 输出:3fmt.Println(cap(s))// 输出:5s[0]=1fmt.Println(s)// 输出:[1 0 0]}直接初始化
packagemainimport"fmt"funcmain(){s:=[]int{1,2,3}fmt.Println(s)// 输出:[1 2 3]fmt.Println(len(s))// 输出:3fmt.Println(cap(s))// 输出:3}Slice 的核心操作
📌 append(重点)
packagemainimport"fmt"funcmain(){s:=[]int{1,2}s=append(s,3,4)fmt.Println(s)// 输出:[1 2 3 4]s=append(s,5)fmt.Println(s)// 输出:[1 2 3 4 5]fmt.Println(len(s))// 输出:5fmt.Println(cap(s))// 输出:8// 为什么输出 8 而不是 5?// 因为 append 函数会将切片扩容,扩容的策略是原来的两倍。s=append(s,6)fmt.Println(len(s))// 输出:6fmt.Println(cap(s))// 输出:8}📌 copy
packagemainimport"fmt"funcmain(){src:=[]int{1,2,3}dst:=make([]int,len(src))copy(dst,src)fmt.Println(dst)// 输出:[1 2 3]fmt.Println(src)// 输出:[1 2 3]fmt.Println(copy(dst,src))// 输出:3fmt.Println(len(dst))// 输出:3fmt.Println(cap(dst))// 输出:3fmt.Println(len(src))// 输出:3fmt.Println(cap(src))// 输出:3fmt.Println(dst[0])// 输出:1fmt.Println(dst[1])// 输出:2fmt.Println(dst[2])// 输出:3}📌 截取(共享底层数组)
packagemainimport"fmt"funcmain(){s:=[]int{1,2,3,4,5}sub:=s[1:3]fmt.Println(len(sub))// 输出:2fmt.Println(cap(sub))// 输出:4sub[0]=100fmt.Println(s)// 输出:[1 100 3 4 5]fmt.Println(sub)// 输出:[100 3]fmt.Println(len(sub))// 输出:2fmt.Println(cap(sub))// 输出:4}👉说明:共享底层数组!
Slice 扩容机制(面试高频🔥)
📌 规则(Go 1.18+)
- 小于 1024:翻倍扩容
- 大于等于 1024:每次增长约 1.25 倍
📌 示例
packagemainimport"fmt"funcmain(){s:=make([]int,0)fori:=0;i<10;i++{s=append(s,i)fmt.Println("长度:",len(s),"容量:",cap(s))}}输出:
长度: 1 容量: 1 长度: 2 容量: 2 长度: 3 容量: 4 长度: 4 容量: 4 长度: 5 容量: 8 长度: 6 容量: 8 长度: 7 容量: 8 长度: 8 容量: 8 长度: 9 容量: 16 长度: 10 容量: 16📌 扩容本质
当容量不够时:
- 创建新数组
- 拷贝旧数据
- 指针指向新数组
Slice 常见坑(非常重要🔥)
❗坑 共享底层数组导致数据污染
packagemainimport"fmt"funcmain(){s1:=[]int{1,2,3}s2:=s1[:2]s2[0]=100fmt.Println(s1)// 被修改 输出:[100 2 3]fmt.Println(s2)// 输出:[100 2]}❗坑 函数传参修改问题
packagemainimport"fmt"funcmodify(s[]int){// 修改切片元素s[0]=100}funcmain(){s:=[]int{1,2,3}modify(s)fmt.Println(s)// 输出:[100 2 3]}👉修改元素可以影响原数据
但:
packagemainimport"fmt"funcappendData(s[]int){// 这里修改的是局部变量s,并不会影响外部的ss=append(s,100)}funcmain(){s:=[]int{1,2,3}appendData(s)fmt.Println(s)// 输出:[1 2 3]}👉 外部不会变!
Slice 最佳实践
提前分配容量
// 创建一个长度为0,容量为1000的切片s:=make([]int,0,1000)👉 避免频繁扩容
避免共享数据污染
// 将切片s1的所有元素追加到空切片s2中s2:=append([]int(nil),s1...)使用 copy 做深拷贝
// 创建一个切片dst:=make([]int,len(src))// 将src切片的内容复制到dst中copy(dst,src)面试高频问题总结
🔥 Q:Slice 是值类型还是引用类型?
👉 本质是值类型,但内部包含指针 → 表现为引用类型
🔥 Q:append 一定会扩容吗?
👉 不一定,容量够就不会
🔥 Q:slice 扩容后原数据还在吗?
👉 在(被 copy 到新数组)
🔥 Q:为什么修改 slice 会影响原数组?
👉 因为共享底层数组
一句话总结
Slice = 指针 + 长度 + 容量
本质是“对数组的引用封装”,灵活但容易踩坑
🚀 结语
Slice 是 Go 中最核心的数据结构之一:
✔ 用得最多
✔ 坑也最多
✔ 面试必问
