切片原理
Go切片底层实现与扩容
概述
切片(slice)是 Go 语言中最常用的数据结构之一,它是对数组的抽象封装,提供了动态扩容的能力。与固定长度的数组不同,切片的长度可以在运行时动态增长。理解切片的底层实现原理,有助于编写高效的代码并避免常见的内存陷阱。
基础概念
切片与数组的区别
数组是固定长度的,长度是类型的一部分。切片是动态的,底层引用一个数组。切片本身只是一个小的结构体,包含指向底层数组的指针、长度和容量。
// 数组:长度是类型的一部分
var arr1 [5]int // 长度为 5 的数组
var arr2 [3]int // 长度为 3 的数组,与 arr1 类型不同
// 切片:长度可变
var s1 []int // nil 切片
s2 := []int{1, 2, 3} // 字面量创建
切片的底层结构
切片在运行时由以下结构体表示:
type slice struct {
array unsafe.Pointer // 指向底层数组的指针
len int // 当前元素个数
cap int // 底层数组的容量
}
这三个字段的大小在 64 位系统上共占 24 字节。切片本身很小,复制切片只是复制这三个字段,底层数组不会被复制。
快速上手
创建切片
// 方式一:字面量
s1 := []int{1, 2, 3}
// 方式二:make 函数
s2 := make([]int, 5) // 长度 5,容量 5
s3 := make([]int, 3, 10) // 长度 3,容量 10
// 方式三:从数组切片
arr := [5]int{1, 2, 3, 4, 5}
s4 := arr[1:3] // [2, 3],len=2, cap=4
// 方式四:new(不推荐,得到的是指向切片的指针)
s5 := *new([]int) // nil 切片
基本操作
s := []int{1, 2, 3}
// 获取长度和容量
fmt.Println(len(s)) // 3
fmt.Println(cap(s)) // 3
// 追加元素
s = append(s, 4) // [1, 2, 3, 4]
// 追加多个元素
s = append(s, 5, 6) // [1, 2, 3, 4, 5, 6]
// 复制切片
dst := make([]int, len(s))
copy(dst, s)
详细用法
扩容策略
当 append 操作导致切片长度超过容量时,Go 运行时会分配新的底层数组并将旧数据复制过去。扩容策略如下:
- 旧容量小于 256:新容量 = 旧容量 * 2(翻倍)
- 旧容量大于等于 256:新容量 = 旧容量 * 1.25 + 192(渐进增长)
- 最终容量还需要根据元素大小进行内存对齐
// 观察扩容过程
func observeGrow() {
var s []int
for i := 0; i < 1000; i++ {
s = append(s, i)
if i%100 == 0 {
fmt.Printf("len=%d cap=%d\n", len(s), cap(s))
}
}
}
切片表达式
Go 提供了简单切片表达式和完整切片表达式两种语法:
arr := [5]int{1, 2, 3, 4, 5}
// 简单切片表达式:s[low:high]
s1 := arr[1:3] // [2, 3], len=2, cap=4
// 省略下界
s2 := arr[:3] // [1, 2, 3], len=3, cap=5
// 省略上界
s3 := arr[2:] // [3, 4, 5], len=3, cap=3
// 完整切片表达式:s[low:high:max]
// 限制容量为 max-low
s4 := arr[1:3:4] // [2, 3], len=2, cap=3
完整切片表达式可以限制新切片的容量,防止 append 操作意外覆盖原数组的后续元素。
切片共享底层数组
这是切片最常见的陷阱:多个切片可能共享同一个底层数组,修改一个切片会影响其他切片。
// 切片共享底层数组
a := []int{1, 2, 3, 4, 5}
b := a[1:3] // b = [2, 3]
b[0] = 99 // a[1] 也变成 99
fmt.Println(a) // [1, 99, 3, 4, 5]
fmt.Println(b) // [99, 3]
使用 copy 避免共享:
a := []int{1, 2, 3, 4, 5}
b := make([]int, 2)
copy(b, a[1:3]) // b 拥有独立的底层数组
b[0] = 99 // 不影响 a
使用完整切片表达式限制容量:
a := []int{1, 2, 3, 4, 5}
b := a[1:3:3] // len=2, cap=2
b = append(b, 10) // 触发扩容,b 获得新底层数组
// a 不受影响
nil 切片与空切片
// nil 切片:未初始化
var s1 []int
fmt.Println(s1 == nil) // true
fmt.Println(len(s1)) // 0
fmt.Println(cap(s1)) // 0
// 空切片:已初始化但无元素
s2 := []int{}
fmt.Println(s2 == nil) // false
fmt.Println(len(s2)) // 0
// 两者在 JSON 序列化时行为不同
// nil 切片序列化为 null
// 空切片序列化为 []
在大多数使用场景下,nil 切片和空切片可以互换,但在 JSON 序列化和反射中行为不同。
常见场景
场景一:预分配切片提升性能
// 不推荐:多次扩容
func buildSliceBad(n int) []int {
var s []int
for i := 0; i < n; i++ {
s = append(s, i) // 可能多次扩容和复制
}
return s
}
// 推荐:预分配容量
func buildSliceGood(n int) []int {
s := make([]int, 0, n) // 预分配容量
for i := 0; i < n; i++ {
s = append(s, i) // 不会触发扩容
}
return s
}
场景二:删除切片元素
// 删除索引 i 处的元素(保持顺序)
func remove(s []int, i int) []int {
return append(s[:i], s[i+1:]...)
}
// 删除索引 i 处的元素(不保持顺序,性能更好)
func removeFast(s []int, i int) []int {
s[i] = s[len(s)-1]
return s[:len(s)-1]
}
场景三:切片去重
func unique(s []int) []int {
seen := make(map[int]bool)
result := make([]int, 0, len(s))
for _, v := range s {
if !seen[v] {
seen[v] = true
result = append(result, v)
}
}
return result
}
场景四:二维切片
// 创建二维切片
matrix := make([][]int, rows)
for i := range matrix {
matrix[i] = make([]int, cols)
}
// 或者使用一维切片模拟二维(性能更好,内存连续)
flat := make([]int, rows*cols)
// 访问 matrix[i][j] 等价于 flat[i*cols+j]
注意事项
- 切片作为函数参数时,传递的是 slice 结构体的副本,但底层数组是共享的。函数内 append 不会影响原切片的 len/cap,但修改元素会影响原切片
- append 返回的新切片必须用原变量接收:
s = append(s, x) - 大切片复制有性能开销,尽量预分配容量避免频繁扩容
- 切片引用大数组的一小部分时,原数组无法被 GC 回收。使用 copy 创建独立切片释放原数组
- 并发读写同一个切片不安全,需要加锁或使用 sync 包
进阶用法
切片与内存泄漏
// 内存泄漏场景:切片引用大数组的尾部
func leak() []int {
data := make([]int, 1000000) // 大数组
return data[:10] // 只返回前 10 个元素
// 但底层数组仍占 1000000 个 int 的内存
}
// 修复:复制到新切片
func noLeak() []int {
data := make([]int, 1000000)
result := make([]int, 10)
copy(result, data[:10])
return result // 原大数组可以被 GC 回收
}
切片排序与搜索
import "sort"
s := []int{3, 1, 4, 1, 5, 9}
// 排序
sort.Ints(s)
// 二分搜索(切片必须已排序)
idx := sort.SearchInts(s, 4)
// 自定义排序
sort.Slice(s, func(i, j int) bool {
return s[i] > s[j] // 降序
})
反射操作切片
import "reflect"
func sliceContains(slice, item interface{}) bool {
s := reflect.ValueOf(slice)
for i := 0; i < s.Len(); i++ {
if reflect.DeepEqual(s.Index(i).Interface(), item) {
return true
}
}
return false
}
// 使用
found := sliceContains([]string{"a", "b", "c"}, "b") // true
泛型切片工具函数
// Go 1.18+ 泛型实现 Map 函数
func Map[T, U any](s []T, fn func(T) U) []U {
result := make([]U, len(s))
for i, v := range s {
result[i] = fn(v)
}
return result
}
// 泛型 Filter 函数
func Filter[T any](s []T, fn func(T) bool) []T {
result := make([]T, 0, len(s))
for _, v := range s {
if fn(v) {
result = append(result, v)
}
}
return result
}
// 使用示例
nums := []int{1, 2, 3, 4, 5}
doubled := Map(nums, func(n int) int { return n * 2 }) // [2, 4, 6, 8, 10]
evens := Filter(nums, func(n int) bool { return n%2 == 0 }) // [2, 4]