Slice的底层实现
1. Slice概述
Slice(切片)是Go语言中最常用的数据结构之一,它是对数组的抽象,提供了动态数组的功能。
1.1 为什么需要Slice?
数组的局限性:
- 固定长度:数组长度在编译期确定,无法动态增长
- 值传递:数组作为参数传递时会复制整个数组
- 不够灵活:难以实现动态数据结构
Slice的优势:
- 动态长度:可以根据需要自动扩容
- 引用传递:传递slice只复制slice头,不复制底层数组
- 灵活操作:支持切片、追加等操作
2. Slice的底层结构
2.1 源码定义
go
// src/runtime/slice.go
type slice struct {
array unsafe.Pointer // 指向底层数组的指针
len int // 当前长度
cap int // 容量
}
2.2 内存布局
Slice Header (24 bytes on 64-bit)
┌─────────────────────────────────┐
│ array (8 bytes) │ ──┐
├─────────────────────────────────┤ │
│ len (8 bytes) │ │
├─────────────────────────────────┤ │
│ cap (8 bytes) │ │
└─────────────────────────────────┘ │
│
▼
Underlying Array
┌───┬───┬───┬───┬───┬───┐
│ 0 │ 1 │ 2 │ 3 │ 4 │ 5 │
└───┴───┴───┴───┴───┴───┘
2.3 示例验证
go
package main
import (
"fmt"
"unsafe"
)
func main() {
s := []int{1, 2, 3, 4, 5}
fmt.Printf("Slice size: %d bytes\n", unsafe.Sizeof(s)) // 24
fmt.Printf("Length: %d\n", len(s)) // 5
fmt.Printf("Capacity: %d\n", cap(s)) // 5
// 获取底层数组指针
fmt.Printf("Array pointer: %p\n", &s[0])
}
3. Slice的创建方式
3.1 字面量创建
go
// 直接初始化
s1 := []int{1, 2, 3, 4, 5}
// 底层:
// 1. 创建一个数组 [5]int{1, 2, 3, 4, 5}
// 2. 创建slice指向该数组
// 3. len = 5, cap = 5
3.2 make函数创建
go
// 指定长度和容量
s2 := make([]int, 5, 10)
// len = 5, cap = 10
// 底层数组大小为10,前5个元素初始化为0
// 只指定长度
s3 := make([]int, 5)
// len = 5, cap = 5
// 底层实现
// src/runtime/slice.go
func makeslice(et *_type, len, cap int) unsafe.Pointer {
mem, overflow := math.MulUintptr(et.size, uintptr(cap))
if overflow || mem > maxAlloc || len < 0 || len > cap {
panic(errorString("makeslice: len out of range"))
}
return mallocgc(mem, et, true)
}
3.3 从数组切片
go
arr := [10]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
s4 := arr[2:7] // [2, 3, 4, 5, 6]
// len = 5 (7-2)
// cap = 8 (10-2,从起始位置到数组末尾)
// 完整切片表达式
s5 := arr[2:7:8] // [low:high:max]
// len = 5 (7-2)
// cap = 6 (8-2)
3.4 从slice切片
go
s := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
s1 := s[2:7]
// 共享底层数组
// s1的修改会影响s
fmt.Println(s) // [0 1 2 3 4 5 6 7 8 9]
s1[0] = 100
fmt.Println(s) // [0 1 100 3 4 5 6 7 8 9]
4. Slice的扩容机制
4.1 扩容触发条件
当len == cap时,执行append操作会触发扩容。
4.2 扩容策略(Go 1.18+)
go
// src/runtime/slice.go
func growslice(et *_type, old slice, cap int) slice {
newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
newcap = cap
} else {
const threshold = 256
if old.cap < threshold {
newcap = doublecap
} else {
// 平滑增长
for 0 < newcap && newcap < cap {
newcap += (newcap + 3*threshold) / 4
}
if newcap <= 0 {
newcap = cap
}
}
}
// 内存对齐
// ...
}
扩容规则总结:
- 小容量(< 256):直接翻倍
- 大容量(≥ 256):增长因子逐渐降低,约1.25倍
- 特殊情况:如果需要的容量超过2倍,直接使用需要的容量
4.3 扩容示例
go
package main
import "fmt"
func main() {
s := make([]int, 0)
oldCap := cap(s)
for i := 0; i < 2048; i++ {
s = append(s, i)
if newCap := cap(s); newCap != oldCap {
fmt.Printf("len: %4d, cap: %4d -> %4d (%.2fx)\n",
len(s)-1, oldCap, newCap, float64(newCap)/float64(oldCap))
oldCap = newCap
}
}
}
// 输出示例:
// len: 0, cap: 0 -> 1 (inf)
// len: 1, cap: 1 -> 2 (2.00x)
// len: 2, cap: 2 -> 4 (2.00x)
// len: 4, cap: 4 -> 8 (2.00x)
// len: 8, cap: 8 -> 16 (2.00x)
// ...
// len: 256, cap: 256 -> 512 (2.00x)
// len: 512, cap: 512 -> 848 (1.66x)
// len: 848, cap: 848 -> 1280 (1.51x)
// len: 1280, cap: 1280 -> 1792 (1.40x)
4.4 扩容的内存分配
go
// 扩容时会:
// 1. 分配新的底层数组
// 2. 复制旧数据到新数组
// 3. 返回新的slice
s := []int{1, 2, 3}
fmt.Printf("Before: %p\n", &s[0])
s = append(s, 4, 5, 6, 7) // 触发扩容
fmt.Printf("After: %p\n", &s[0]) // 地址改变
5. Slice的操作
5.1 append操作
go
// 单个元素
s := []int{1, 2, 3}
s = append(s, 4) // [1, 2, 3, 4]
// 多个元素
s = append(s, 5, 6, 7) // [1, 2, 3, 4, 5, 6, 7]
// 追加另一个slice
s2 := []int{8, 9}
s = append(s, s2...) // [1, 2, 3, 4, 5, 6, 7, 8, 9]
// 底层实现
// src/runtime/slice.go
func append(slice []Type, elems ...Type) []Type
5.2 copy操作
go
src := []int{1, 2, 3, 4, 5}
dst := make([]int, 3)
n := copy(dst, src) // 复制3个元素
fmt.Println(dst) // [1, 2, 3]
fmt.Println(n) // 3
// copy的长度是min(len(dst), len(src))
// 底层实现使用memmove
// src/runtime/slice.go
func slicecopy(toPtr unsafe.Pointer, toLen int,
fromPtr unsafe.Pointer, fromLen int, width uintptr) int {
if fromLen == 0 || toLen == 0 {
return 0
}
n := fromLen
if toLen < n {
n = toLen
}
size := uintptr(n) * width
memmove(toPtr, fromPtr, size)
return n
}
5.3 切片操作
go
s := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
// 基本切片
s1 := s[2:7] // [2, 3, 4, 5, 6]
s2 := s[:5] // [0, 1, 2, 3, 4]
s3 := s[5:] // [5, 6, 7, 8, 9]
s4 := s[:] // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
// 完整切片表达式(限制容量)
s5 := s[2:7:7] // len=5, cap=5
5.4 删除元素
Go没有内置的删除函数,需要手动实现:
go
// 删除索引i的元素
func remove(slice []int, i int) []int {
return append(slice[:i], slice[i+1:]...)
}
// 删除索引i的元素(保持顺序,更高效)
func removeKeepOrder(slice []int, i int) []int {
copy(slice[i:], slice[i+1:])
return slice[:len(slice)-1]
}
// 删除索引i的元素(不保持顺序,最高效)
func removeFast(slice []int, i int) []int {
slice[i] = slice[len(slice)-1]
return slice[:len(slice)-1]
}
// 使用示例
s := []int{1, 2, 3, 4, 5}
s = remove(s, 2) // [1, 2, 4, 5]
5.5 插入元素
go
// 在索引i处插入元素
func insert(slice []int, i int, value int) []int {
// 扩展slice
slice = append(slice, 0)
// 移动元素
copy(slice[i+1:], slice[i:])
// 插入新值
slice[i] = value
return slice
}
// 使用示例
s := []int{1, 2, 4, 5}
s = insert(s, 2, 3) // [1, 2, 3, 4, 5]
6. Slice的陷阱
6.1 共享底层数组
go
s1 := []int{1, 2, 3, 4, 5}
s2 := s1[1:3] // [2, 3]
s2[0] = 100
fmt.Println(s1) // [1, 100, 3, 4, 5] - s1也被修改了!
解决方案:使用copy创建独立副本
go
s1 := []int{1, 2, 3, 4, 5}
s2 := make([]int, 2)
copy(s2, s1[1:3])
s2[0] = 100
fmt.Println(s1) // [1, 2, 3, 4, 5] - s1不受影响
6.2 append导致的意外
go
s := []int{1, 2, 3, 4, 5}
s1 := s[0:2] // [1, 2], cap=5
s2 := s[2:4] // [3, 4], cap=3
s1 = append(s1, 100) // 修改了s[2]
fmt.Println(s) // [1, 2, 100, 4, 5]
fmt.Println(s2) // [100, 4] - s2也受影响!
解决方案:使用完整切片表达式限制容量
go
s := []int{1, 2, 3, 4, 5}
s1 := s[0:2:2] // len=2, cap=2
s1 = append(s1, 100) // 触发扩容,分配新数组
fmt.Println(s) // [1, 2, 3, 4, 5] - s不受影响
6.3 循环中的append
go
// 错误示例
s := []int{1, 2, 3}
for _, v := range s {
s = append(s, v) // 可能导致无限循环或意外行为
}
// 正确做法
s := []int{1, 2, 3}
length := len(s)
for i := 0; i < length; i++ {
s = append(s, s[i])
}
6.4 slice作为函数参数
go
func modify(s []int) {
s[0] = 100 // 会修改原slice的元素
s = append(s, 200) // 不会影响原slice(如果触发扩容)
}
func main() {
s := []int{1, 2, 3}
modify(s)
fmt.Println(s) // [100, 2, 3]
}
// 如果需要修改slice本身,使用指针
func modifySlice(s *[]int) {
*s = append(*s, 200)
}
7. 性能优化
7.1 预分配容量
go
// 不好:多次扩容
s := []int{}
for i := 0; i < 10000; i++ {
s = append(s, i)
}
// 好:预分配容量
s := make([]int, 0, 10000)
for i := 0; i < 10000; i++ {
s = append(s, i)
}
7.2 避免不必要的复制
go
// 不好:每次都复制
func process(s []int) {
temp := make([]int, len(s))
copy(temp, s)
// 处理temp...
}
// 好:直接使用
func process(s []int) {
// 直接处理s...
}
7.3 使用索引而非range
go
// range会复制元素
for _, v := range largeSlice {
process(v) // v是副本
}
// 使用索引避免复制
for i := 0; i < len(largeSlice); i++ {
process(largeSlice[i])
}
7.4 重用slice
go
// 不好:频繁分配
for i := 0; i < 1000; i++ {
s := make([]int, 100)
// 使用s...
}
// 好:重用slice
s := make([]int, 100)
for i := 0; i < 1000; i++ {
s = s[:0] // 重置长度,保留容量
// 使用s...
}
8. Slice与数组的对比
| 特性 | 数组 | Slice |
|---|---|---|
| 长度 | 固定,编译期确定 | 动态,运行时可变 |
| 传递 | 值传递,复制整个数组 | 引用传递,只复制slice头 |
| 大小 | 取决于元素数量 | 固定24字节(64位) |
| 比较 | 可以用==比较 | 不能用==比较(除了nil) |
| 零值 | 所有元素为零值 | nil |
9. 实战示例
9.1 实现一个动态栈
go
type Stack struct {
data []int
}
func (s *Stack) Push(v int) {
s.data = append(s.data, v)
}
func (s *Stack) Pop() (int, bool) {
if len(s.data) == 0 {
return 0, false
}
v := s.data[len(s.data)-1]
s.data = s.data[:len(s.data)-1]
return v, true
}
func (s *Stack) Peek() (int, bool) {
if len(s.data) == 0 {
return 0, false
}
return s.data[len(s.data)-1], true
}
9.2 实现去重
go
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
}
9.3 实现过滤
go
func filter(s []int, f func(int) bool) []int {
result := make([]int, 0, len(s))
for _, v := range s {
if f(v) {
result = append(result, v)
}
}
return result
}
// 使用
evens := filter([]int{1, 2, 3, 4, 5}, func(n int) bool {
return n%2 == 0
}) // [2, 4]
10. 总结
- Slice是对数组的抽象,由指针、长度和容量组成
- Slice的扩容策略在Go 1.18后进行了优化,更加平滑
- 多个slice可能共享底层数组,修改时需要注意
- 预分配容量可以显著提高性能
- 理解slice的底层实现有助于避免常见陷阱