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