切片的基本概念
切片(Slice)是Go语言中一种重要的数据结构,它是对数组的抽象,提供了更灵活、更强大的序列操作接口。切片由三个部分组成:
指针:指向底层数组的起始位置
这个指针实际上是一个内存地址,指向切片第一个元素在底层数组中的位置。例如:
- 如果底层数组是
[0,1,2,3,4]
- 切片
[1:3]
的指针将指向元素1的位置 - 在内存中,这个指针是一个8字节的值(64位系统)
长度(length):切片中元素的个数
- 可以通过内置函数
len()
获取 - 长度决定了切片可以访问的元素范围
- 长度必须小于等于容量
- 尝试访问超出长度的元素会导致panic
容量(capacity):从切片开始位置到底层数组结束位置的元素个数
- 可以通过内置函数
cap()
获取 - 容量决定了切片可以扩展的最大限度
- 容量必须大于等于长度
- 当使用append操作时,如果长度超过容量,会触发扩容
切片与数组的区别
数组
- 固定长度,声明时需要指定大小
- 长度是类型的一部分,
[3]int
和[5]int
是不同的类型
- 长度是类型的一部分,
- 值类型,赋值和传参会复制整个数组
- 传递大数组会有性能开销
- 内存分配在栈上(小数组)或堆上(大数组)
- 通常长度小于等于4KB的数组会分配在栈上
- 示例:
var arr1 [5]int // 声明一个长度为5的int数组 arr2 := [3]string{"a", "b", "c"} // 声明并初始化 arr3 := [...]int{1, 2, 3} // 编译器推导长度
切片
- 动态长度,不需要指定大小
- 长度可以在运行时改变
- 引用类型,赋值和传参只复制切片头(指针、长度、容量)
- 复制切片的开销很小(24字节)
- 内存分配总是在堆上
- 因为需要在运行时动态调整大小
- 示例:
var s1 []int // 声明一个int切片 s2 := make([]float64, 3) // 使用make创建 s3 := []string{"x", "y", "z"} // 直接初始化
切片的创建方式
1. 从数组创建切片
arr := [5]int{1, 2, 3, 4, 5}
// 基本切片操作
slice1 := arr[1:3] // 包含arr[1], arr[2],长度2,容量4
slice2 := arr[:4] // 从开始到索引3,长度4,容量5
slice3 := arr[2:] // 从索引2到最后,长度3,容量3
// 完整切片表达式(控制容量)
slice4 := arr[1:3:4] // 长度2,容量3(4-1)
2. 使用make函数创建
// 创建一个长度为3,容量为5的int切片
s := make([]int, 3, 5)
// 只指定长度(容量=长度)
s2 := make([]string, 2) // 长度和容量都是2
// 常见用途:预分配空间避免频繁扩容
names := make([]string, 0, 100) // 初始长度0,容量100
3. 直接初始化
// 初始化带值的切片
s1 := []int{1, 2, 3} // 长度和容量都是3
s2 := []string{"Go", "Python", "Java"}
// 空切片
empty1 := []int{} // 空切片,长度和容量为0
var empty2 []float64 // nil切片
// 多维切片
matrix := [][]int{
{1, 2, 3},
{4, 5, 6},
}
切片操作详解
1. 追加元素(append)
// 基本追加
s := []int{1, 2, 3}
s = append(s, 4) // [1,2,3,4]
s = append(s, 5, 6) // [1,2,3,4,5,6]
// 追加另一个切片
s2 := []int{7, 8, 9}
s = append(s, s2...) // [1,2,3,4,5,6,7,8,9]
// 扩容观察
var nums []int
for i := 0; i < 10; i++ {
fmt.Printf("len=%d cap=%d\n", len(nums), cap(nums))
nums = append(nums, i)
}
2. 切片复制(copy)
// 基本复制
src := []int{1, 2, 3}
dst := make([]int, 2)
n := copy(dst, src) // dst = [1, 2], n = 2
// 重叠复制
s := []int{0, 1, 2, 3, 4}
copy(s[1:], s[2:]) // s = [0,2,3,4,4]
// 完全复制
original := []string{"a", "b", "c"}
clone := make([]string, len(original))
copy(clone, original)
3. 切片截取
s := []int{0, 1, 2, 3, 4}
// 基本截取
s1 := s[1:3] // [1, 2]
s2 := s[:3] // [0, 1, 2]
s3 := s[3:] // [3, 4]
// 带容量的截取
s4 := s[1:3:4] // [1,2] 长度2,容量3
// 修改切片会影响底层数组
s1[0] = 9 // s = [0,9,2,3,4]
切片底层原理
切片实际上是一个结构体,可以表示为:
type slice struct {
array unsafe.Pointer // 指向底层数组的指针
len int // 切片长度
cap int // 切片容量
}
运行时行为:
- 当传递切片给函数时,实际上是传递了这个结构体的副本
- 切片操作不会复制底层数组,除非扩容发生
- 多个切片可以共享同一个底层数组
内存布局示例:
底层数组: [0,1,2,3,4,5,6,7,8,9]
切片s := array[2:5] // len=3, cap=8
内存表示:
slice {
array: &array[2],
len: 3,
cap: 8
}
扩容机制
当切片容量不足时,Go会按照以下规则扩容:
基本规则:
- 如果新容量大于当前容量的2倍,则使用新容量
- 否则,如果当前切片长度小于1024,则容量翻倍
- 如果当前切片长度大于等于1024,则每次增加25%
扩容示例:
var s []int
for i := 0; i < 10; i++ {
fmt.Printf("len=%d cap=%d\n", len(s), cap(s))
s = append(s, i)
}
输出可能:
len=0 cap=0
len=1 cap=1
len=2 cap=2
len=3 cap=4
len=4 cap=4
len=5 cap=8
...
扩容性能影响:
- 每次扩容都需要分配新内存和复制数据
- 频繁扩容会导致性能下降
- 预分配足够容量可以避免不必要的扩容
常见使用场景
1. 动态数组
// 动态增长的数组
var nums []int
for i := 0; i < 1000; i++ {
nums = append(nums, i*2)
}
// 预分配版本(更高效)
nums := make([]int, 0, 1000)
for i := 0; i < 1000; i++ {
nums = append(nums, i*2)
}
2. 字符串处理
// 字符串切片
str := "hello, world"
sub := str[7:12] // "world"
// 字符串与字节切片转换
bytes := []byte("hello")
str = string(bytes)
// 高效字符串处理
func toUpper(s string) string {
b := make([]byte, len(s))
for i := range b {
c := s[i]
if c >= 'a' && c <= 'z' {
c -= 'a' - 'A'
}
b[i] = c
}
return string(b)
}
3. 函数参数传递
// 处理可变长度数据
func sum(nums []int) int {
total := 0
for _, n := range nums {
total += n
}
return total
}
// 修改切片内容
func addOne(nums []int) {
for i := range nums {
nums[i]++
}
}
// 返回新切片
func filterEven(nums []int) []int {
var result []int
for _, n := range nums {
if n%2 == 0 {
result = append(result, n)
}
}
return result
}
性能优化建议
预分配容量:
// 不好:频繁扩容
var s []int
for i := 0; i < 1000; i++ {
s = append(s, i)
}
// 好:预分配容量
s := make([]int, 0, 1000)
for i := 0; i < 1000; i++ {
s = append(s, i)
}
避免大切片复制:
// 不好:复制大数据
bigData := make([]byte, 10*1024*1024) // 10MB
copyData := make([]byte, len(bigData))
copy(copyData, bigData)
// 好:使用指针
type BigData struct {
data []byte
}
bd := &BigData{data: make([]byte, 10*1024*1024)}
复用切片:
// 复用切片内存
var buffer []byte
func process(data []byte) {
buffer = buffer[:0] // 清空但保留底层数组
buffer = append(buffer, data...)
// 处理buffer
}
常见陷阱
共享底层数组:
a := []int{1, 2, 3, 4}
b := a[:2] // [1, 2]
b[0] = 9 // a = [9, 2, 3, 4]
// 解决方案:需要独立副本时使用copy
c := make([]int, 2)
copy(c, a[:2])
c[0] = 8 // a不受影响
内存泄漏:
func getLastNums() []int {
bigSlice := make([]int, 1000000)
return bigSlice[len(bigSlice)-10:] // 只返回最后10个,但整个底层数组不会被释放
}
// 解决方案:显式复制需要的数据
func getLastNumsSafe() []int {
bigSlice := make([]int, 1000000)
result := make([]int, 10)
copy(result, bigSlice[len(bigSlice)-10:])
return result
}
nil切片 vs 空切片:
var nilSlice []int // nil切片,指针为nil
emptySlice := []int{} // 空切片,指针不为nil
// 行为差异
fmt.Println(nilSlice == nil) // true
fmt.Println(emptySlice == nil) // false
// 但都可以正常使用len和append
fmt.Println(len(nilSlice)) // 0
fmt.Println(len(emptySlice)) // 0
nilSlice = append(nilSlice, 1) // 可以append