Go Slice 详解 Slice(切片)是 Go 语言中最重要的数据结构之一,它提供了对数组的动态视图。理解 slice 的底层实现对于编写高效、正确的 Go 程序至关重要。
目录
Slice 概述 什么是 Slice Slice 是对数组的抽象,提供了对数组元素的动态访问。Slice 本身不存储数据,而是引用底层数组的一部分。
graph TB
A[Slice] --> B[底层数组]
A --> C[指针 ptr]
A --> D[长度 len]
A --> E[容量 cap]
C --> B1[指向数组元素]
D --> D1[当前元素数量]
E --> E1[可容纳元素数量]
style A fill:#ffcccc
style B fill:#ccffcc
Slice 的特点
动态长度 :可以在运行时改变长度
引用语义 :多个 slice 可以共享同一个底层数组
零值可用 :nil slice 可以直接使用
类型安全 :编译时检查类型
Slice vs Array
特性
Array
Slice
长度
固定
可变
类型
[n]T
[]T
值传递
值拷贝
引用传递
零值
零值数组
nil
使用场景
固定大小数据
动态大小数据
Slice 的底层结构 运行时表示 Slice 在运行时由三个字段组成:
1 2 3 4 5 type slice struct { ptr unsafe.Pointer len int cap int }
字段说明 ptr(指针)
指向底层数组的第一个元素
如果 slice 为 nil,ptr 为 nil
多个 slice 可以指向同一个底层数组
len(长度)
当前 slice 中元素的数量
可以通过 len(slice) 获取
长度不能超过容量
cap(容量)
底层数组从 ptr 开始到数组末尾的元素数量
可以通过 cap(slice) 获取
容量 >= 长度
内存布局示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package mainimport ( "fmt" "unsafe" ) func main () { arr := [5 ]int {1 , 2 , 3 , 4 , 5 } s := arr[1 :4 ] sPtr := (*[3 ]int )(unsafe.Pointer(&s)) fmt.Printf("Slice header: %+v\n" , sPtr) fmt.Printf("Length: %d\n" , len (s)) fmt.Printf("Capacity: %d\n" , cap (s)) fmt.Printf("Elements: %v\n" , s) }
可视化表示 1 2 3 4 5 6 7 底层数组: [1, 2, 3, 4, 5] ↑ ↑ ↑ | | | ptr len=3 cap=4 Slice: {ptr: &arr[1], len: 3, cap: 4} 实际元素: [2, 3, 4]
Slice 的创建和初始化 声明 Slice 1 2 3 4 5 6 7 8 9 var s []int fmt.Println(s == nil ) var s1 []int = []int {1 , 2 , 3 }s2 := []int {1 , 2 , 3 }
使用 make 创建 1 2 3 4 5 6 s := make ([]int , 5 ) s := make ([]int , 5 , 10 ) s := make ([]int , 3 )
从数组创建 1 2 3 4 5 6 7 8 9 10 11 12 13 arr := [5 ]int {1 , 2 , 3 , 4 , 5 } s1 := arr[:] s2 := arr[1 :] s3 := arr[:3 ] s4 := arr[1 :3 ]
从另一个 Slice 创建 1 2 3 s1 := []int {1 , 2 , 3 , 4 , 5 } s2 := s1[1 :3 ] s3 := s1[:]
空 Slice vs Nil Slice 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 var s1 []int fmt.Println(s1 == nil ) fmt.Println(len (s1)) fmt.Println(cap (s1)) s2 := []int {} fmt.Println(s2 == nil ) fmt.Println(len (s2)) fmt.Println(cap (s2)) s3 := make ([]int , 0 ) fmt.Println(s3 == nil ) fmt.Println(len (s3)) fmt.Println(cap (s3)) s4 := make ([]int , 0 , 10 ) fmt.Println(s4 == nil ) fmt.Println(len (s4)) fmt.Println(cap (s4))
注意 :nil slice 和空 slice 在大多数操作中行为相同,但 nil slice 可以与 nil 比较。
初始化方式对比 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 s1 := []int {1 , 2 , 3 } s2 := make ([]int , 3 ) for i := range s2 { s2[i] = i + 1 } s3 := make ([]int , 0 , 3 ) s3 = append (s3, 1 , 2 , 3 ) arr := [3 ]int {1 , 2 , 3 } s4 := arr[:]
Slice 的扩容机制 扩容触发条件 当 slice 的长度达到容量时,继续 append 会触发扩容:
1 2 3 s := make ([]int , 0 , 2 ) s = append (s, 1 , 2 ) s = append (s, 3 )
扩容策略 Go 1.18+ 的扩容策略:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 func growslice (oldCap, newLen int ) int { newCap := oldCap doubleCap := newCap + newCap if newLen > doubleCap { newCap = newLen } else { if oldCap < 256 { newCap = doubleCap } else { for 0 < newCap && newCap < newLen { newCap += (newCap + 3 *256 ) / 4 } if newCap <= 0 { newCap = newLen } } } return roundupsize(newCap) }
扩容示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package mainimport "fmt" func main () { s := make ([]int , 0 ) for i := 0 ; i < 20 ; i++ { oldCap := cap (s) s = append (s, i) newCap := cap (s) if oldCap != newCap { fmt.Printf("len=%d, cap=%d -> cap=%d (增长 %.2f 倍)\n" , len (s)-1 , oldCap, newCap, float64 (newCap)/float64 (oldCap)) } } }
输出示例:
1 2 3 4 5 6 len=0, cap=0 -> cap=1 (增长 +Inf 倍) len=1, cap=1 -> cap=2 (增长 2.00 倍) len=2, cap=2 -> cap=4 (增长 2.00 倍) len=4, cap=4 -> cap=8 (增长 2.00 倍) len=8, cap=8 -> cap=16 (增长 2.00 倍) len=16, cap=16 -> cap=32 (增长 2.00 倍)
扩容后的内存分配 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package mainimport "fmt" func main () { s1 := []int {1 , 2 , 3 } fmt.Printf("s1: %p, len=%d, cap=%d\n" , s1, len (s1), cap (s1)) s1 = append (s1, 4 ) fmt.Printf("s1: %p, len=%d, cap=%d\n" , s1, len (s1), cap (s1)) s2 := make ([]int , 0 , 10 ) fmt.Printf("s2: %p, len=%d, cap=%d\n" , s2, len (s2), cap (s2)) s2 = append (s2, 1 , 2 , 3 ) fmt.Printf("s2: %p, len=%d, cap=%d\n" , s2, len (s2), cap (s2)) }
内存对齐 扩容后的容量会进行内存对齐,实际容量可能大于计算值:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package mainimport "fmt" func main () { s1 := make ([]int , 0 ) for i := 0 ; i < 10 ; i++ { s1 = append (s1, i) fmt.Printf("len=%d, cap=%d\n" , len (s1), cap (s1)) } s2 := make ([]string , 0 ) for i := 0 ; i < 10 ; i++ { s2 = append (s2, fmt.Sprintf("%d" , i)) fmt.Printf("len=%d, cap=%d\n" , len (s2), cap (s2)) } }
Slice 的截取和复制 Slice 截取(Slicing) 截取操作创建新的 slice,但共享底层数组:
1 2 3 s := []int {1 , 2 , 3 , 4 , 5 } s1 := s[1 :3 ] s2 := s[2 :4 ]
截取语法 1 2 3 4 s[start:end] s[start:] s[:end] s[:]
容量计算 1 2 3 4 5 s := []int {1 , 2 , 3 , 4 , 5 } s1 := s[1 :3 ]
共享底层数组 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package mainimport "fmt" func main () { s := []int {1 , 2 , 3 , 4 , 5 } s1 := s[1 :3 ] s[1 ] = 99 fmt.Println(s1) s1[0 ] = 88 fmt.Println(s) }
Slice 复制 copy 函数 1 func copy (dst, src []T) int
copy 函数将 src 的元素复制到 dst,返回复制的元素数量。
1 2 3 4 s1 := []int {1 , 2 , 3 , 4 , 5 } s2 := make ([]int , 3 ) n := copy (s2, s1)
完整复制 1 2 3 4 5 6 7 8 9 10 s1 := []int {1 , 2 , 3 , 4 , 5 } s2 := make ([]int , len (s1)) copy (s2, s1)s3 := append ([]int (nil ), s1...) s4 := append (make ([]int , 0 , len (s1)), s1...)
部分复制 1 2 3 4 5 6 7 8 9 s1 := []int {1 , 2 , 3 , 4 , 5 } s2 := make ([]int , 3 ) copy (s2, s1) s3 := make ([]int , 3 ) copy (s3, s1[2 :])
复制 vs 截取 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package mainimport "fmt" func main () { s := []int {1 , 2 , 3 , 4 , 5 } s1 := s[1 :3 ] s1[0 ] = 99 fmt.Println(s) s2 := make ([]int , 2 ) copy (s2, s[1 :3 ]) s2[0 ] = 88 fmt.Println(s) fmt.Println(s2) }
深拷贝 vs 浅拷贝 对于包含引用类型的 slice,需要注意深拷贝:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 package mainimport "fmt" func main () { s1 := [][]int {{1 , 2 }, {3 , 4 }} s2 := make ([][]int , len (s1)) copy (s2, s1) s2[0 ][0 ] = 99 fmt.Println(s1) fmt.Println(s2) s3 := make ([][]int , len (s1)) for i := range s1 { s3[i] = make ([]int , len (s1[i])) copy (s3[i], s1[i]) } s3[0 ][0 ] = 88 fmt.Println(s1) fmt.Println(s3) }
Slice 与数组的关系 数组转 Slice 1 2 3 4 5 6 7 8 arr := [5 ]int {1 , 2 , 3 , 4 , 5 } s1 := arr[:] s2 := arr[1 :4 ]
Slice 转数组(Go 1.20+) 1 2 3 4 5 6 7 8 9 10 11 s := []int {1 , 2 , 3 , 4 , 5 } arr := [5 ]int (s) if len (s) >= 5 { arr := [5 ]int (s[:5 ]) }
底层数组共享 多个 slice 可以共享同一个底层数组:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package mainimport "fmt" func main () { arr := [5 ]int {1 , 2 , 3 , 4 , 5 } s1 := arr[0 :3 ] s2 := arr[2 :5 ] fmt.Printf("s1: %v\n" , s1) fmt.Printf("s2: %v\n" , s2) s1[2 ] = 99 fmt.Printf("s1: %v\n" , s1) fmt.Printf("s2: %v\n" , s2) fmt.Printf("arr: %v\n" , arr) }
数组 vs Slice 的性能 1 2 3 4 5 6 7 8 9 func processArray (arr [1000]int ) { } func processSlice (s []int ) { }
Slice 的内存布局 1 2 3 4 +--------+--------+--------+ | ptr | len | cap | +--------+--------+--------+ 8字节 8字节 8字节
完整内存布局 1 2 3 4 5 6 7 8 9 10 Slice Header (24 字节) ├── ptr (8 字节) -> 指向底层数组 ├── len (8 字节) -> 长度 └── cap (8 字节) -> 容量 底层数组 ├── [0] -> 元素 0 ├── [1] -> 元素 1 ├── [2] -> 元素 2 └── ...
内存对齐 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 package mainimport ( "fmt" "unsafe" ) func main () { var s []int fmt.Printf("Slice header size: %d bytes\n" , unsafe.Sizeof(s)) fmt.Printf("ptr offset: %d\n" , unsafe.Offsetof((*struct { ptr unsafe.Pointer len int cap int })(nil ).ptr)) fmt.Printf("len offset: %d\n" , unsafe.Offsetof((*struct { ptr unsafe.Pointer len int cap int })(nil ).len )) fmt.Printf("cap offset: %d\n" , unsafe.Offsetof((*struct { ptr unsafe.Pointer len int cap int })(nil ).cap )) }
内存分配示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package mainimport "fmt" func main () { s1 := make ([]int , 0 , 10 ) fmt.Printf("Small slice: len=%d, cap=%d\n" , len (s1), cap (s1)) s2 := make ([]int , 0 , 1000000 ) fmt.Printf("Large slice: len=%d, cap=%d\n" , len (s2), cap (s2)) s3 := []int {1 , 2 , 3 } fmt.Printf("Before append: len=%d, cap=%d\n" , len (s3), cap (s3)) s3 = append (s3, 4 ) fmt.Printf("After append: len=%d, cap=%d\n" , len (s3), cap (s3)) }
Slice 的性能考虑 预分配容量 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 func slow () []int { s := []int {} for i := 0 ; i < 1000 ; i++ { s = append (s, i) } return s } func fast () []int { s := make ([]int , 0 , 1000 ) for i := 0 ; i < 1000 ; i++ { s = append (s, i) } return s }
避免不必要的复制 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 func process (s []int ) { s2 := make ([]int , len (s)) copy (s2, s) } func process (s []int ) { } func process (s []int ) []int { s2 := make ([]int , len (s)) copy (s2, s) return s2 }
批量操作 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 func slow () []int { s := []int {} for i := 0 ; i < 100 ; i++ { s = append (s, i) } return s } func fast () []int { s := make ([]int , 0 , 100 ) for i := 0 ; i < 100 ; i++ { s = append (s, i) } return s } func faster () []int { s := make ([]int , 100 ) for i := 0 ; i < 100 ; i++ { s[i] = i } return s }
内存泄漏 1 2 3 4 5 6 7 8 9 10 11 12 13 14 func getLargeSlice () []int { large := make ([]int , 1000000 ) return large[:10 ] } func getLargeSlice () []int { large := make ([]int , 1000000 ) result := make ([]int , 10 ) copy (result, large[:10 ]) return result }
性能基准测试 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 package mainimport "testing" func BenchmarkAppendWithoutCap (b *testing.B) { for i := 0 ; i < b.N; i++ { s := []int {} for j := 0 ; j < 1000 ; j++ { s = append (s, j) } } } func BenchmarkAppendWithCap (b *testing.B) { for i := 0 ; i < b.N; i++ { s := make ([]int , 0 , 1000 ) for j := 0 ; j < 1000 ; j++ { s = append (s, j) } } } func BenchmarkDirectAssignment (b *testing.B) { for i := 0 ; i < b.N; i++ { s := make ([]int , 1000 ) for j := 0 ; j < 1000 ; j++ { s[j] = j } } }
预期结果:DirectAssignment > AppendWithCap > AppendWithoutCap
Slice 的常见陷阱 陷阱 1: 共享底层数组 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 func main () { s1 := []int {1 , 2 , 3 , 4 , 5 } s2 := s1[1 :3 ] s2[0 ] = 99 fmt.Println(s1) } func main () { s1 := []int {1 , 2 , 3 , 4 , 5 } s2 := make ([]int , 2 ) copy (s2, s1[1 :3 ]) s2[0 ] = 99 fmt.Println(s1) }
陷阱 2: Append 后 slice 可能指向新数组 1 2 3 4 5 6 7 8 9 10 11 12 13 func main () { s1 := []int {1 , 2 , 3 } s2 := s1[1 :3 ] s2 = append (s2, 4 ) s2[0 ] = 99 fmt.Println(s1) fmt.Println(s2) }
陷阱 3: 在循环中使用 append 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 func process (items []Item) { var results []Item for _, item := range items { if item.Valid { results = append (results, item) } } } func process (items []Item) { results := make ([]Item, 0 , len (items)) for _, item := range items { if item.Valid { results = append (results, item) } } }
陷阱 4: 对 nil slice 的操作 1 2 3 4 5 6 7 8 9 var s []int fmt.Println(len (s)) fmt.Println(cap (s)) s = append (s, 1 ) fmt.Println(s)
陷阱 5: Slice 作为函数参数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 func modify (s []int ) { s = append (s, 99 ) } func main () { s := []int {1 , 2 , 3 } modify(s) fmt.Println(s) } func modify (s []int ) []int { return append (s, 99 ) } func main () { s := []int {1 , 2 , 3 } s = modify(s) fmt.Println(s) } func modify (s []int ) { if len (s) > 0 { s[0 ] = 99 } }
陷阱 6: 截取后容量计算错误 1 2 3 4 5 s := []int {1 , 2 , 3 , 4 , 5 } s1 := s[1 :3 ]
陷阱 7: 内存泄漏 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 func getLastN (items []Item, n int ) []Item { if len (items) < n { n = len (items) } return items[len (items)-n:] } func getLastN (items []Item, n int ) []Item { if len (items) < n { n = len (items) } result := make ([]Item, n) copy (result, items[len (items)-n:]) return result }
陷阱 8: 二维 Slice 的共享 1 2 3 4 5 6 7 8 9 10 11 12 13 14 matrix := make ([][]int , 3 ) row := make ([]int , 3 ) for i := range matrix { matrix[i] = row } matrix[0 ][0 ] = 1 fmt.Println(matrix[1 ][0 ]) matrix := make ([][]int , 3 ) for i := range matrix { matrix[i] = make ([]int , 3 ) }
最佳实践 1. 预分配容量 1 2 s := make ([]int , 0 , expectedSize)
2. 使用零值 slice 1 2 3 var s []int s = append (s, 1 , 2 , 3 )
3. 避免不必要的复制 1 2 3 4 5 6 7 8 9 10 11 12 func process (s []int ) { } func process (s []int ) []int { result := make ([]int , len (s)) copy (result, s) return result }
4. 使用 copy 而不是循环 1 2 3 4 5 6 7 8 9 s2 := make ([]int , len (s1)) for i := range s1 { s2[i] = s1[i] } s2 := make ([]int , len (s1)) copy (s2, s1)
5. 检查 slice 是否为空 1 2 3 4 5 6 7 8 9 10 11 if len (s) == 0 { } if s == nil { } else if len (s) == 0 { }
6. 使用 slice 作为栈 1 2 3 4 5 6 7 8 9 10 11 var stack []int stack = append (stack, value) if len (stack) > 0 { value := stack[len (stack)-1 ] stack = stack[:len (stack)-1 ] }
7. 使用 slice 作为队列(低效) 1 2 3 4 5 6 7 8 9 10 11 var queue []int queue = append (queue, value) if len (queue) > 0 { value := queue[0 ] queue = queue[1 :] }
8. 清空 slice
总结 Slice 是 Go 语言中最重要的数据结构之一:
核心要点
底层结构 :ptr、len、cap 三个字段
引用语义 :多个 slice 可以共享底层数组
动态扩容 :append 时可能触发扩容
内存布局 :slice header + 底层数组
关键理解
Slice 不存储数据,只引用底层数组
截取操作共享底层数组
Append 可能创建新的底层数组
容量和长度的区别
最佳实践
预分配容量以提高性能
避免不必要的复制
注意共享底层数组的影响
防止内存泄漏
掌握 slice 的底层实现和特性,能够编写更高效、更安全的 Go 代码。
参考文献