golang-基础-Go语言切片
本文最后更新于 2025-07-30,文章内容可能已经过时。
Go语言中的切片(Slice)是处理动态数据集合的核心工具,它基于数组构建,但提供了更灵活的动态扩容和操作能力。以下是关于Go语言切片的详细解析,涵盖其结构、创建方式、操作、性能优化及常见陷阱,并附有代码示例。
1. 切片的结构
切片底层由一个结构体表示,包含三个关键字段:
type slice struct {
array unsafe.Pointer // 指向底层数组的指针
len int // 切片的当前元素个数(长度)
cap int // 切片的容量(底层数组的总大小)
}
array
:指向底层数组的指针,切片本身不存储数据,而是引用数组的连续片段。len
:表示当前切片中可用元素的数量。cap
:表示底层数组的总容量,通常cap >= len
。
2. 切片的创建
2.1 使用字面量
直接通过字面量初始化切片:
slice := []int{1, 2, 3, 4, 5}
fmt.Println("slice:", slice, "len:", len(slice), "cap:", cap(slice))
输出:
slice: [1 2 3 4 5] len: 5 cap: 5
2.2 使用 make
函数
通过 make
函数创建切片,可以指定长度和容量:
slice := make([]int, 3, 5) // 长度为3,容量为5
fmt.Println("slice:", slice, "len:", len(slice), "cap:", cap(slice))
输出:
slice: [0 0 0] len: 3 cap: 5
2.3 从数组或切片切割
通过切割数组或切片生成新的切片:
arr := [5]int{1, 2, 3, 4, 5}
slice1 := arr[1:4] // 从索引1到3(不包含4)
slice2 := slice1[0:2] // 从切片中再切割
fmt.Println("slice1:", slice1, "len:", len(slice1), "cap:", cap(slice1))
fmt.Println("slice2:", slice2, "len:", len(slice2), "cap:", cap(slice2))
输出:
slice1: [2 3 4] len: 3 cap: 4
slice2: [2 3] len: 2 cap: 4
3. 切片的操作
3.1 访问元素
通过索引访问切片元素(索引范围:0
到 len-1
):
slice := []int{10, 20, 30}
fmt.Println("第一个元素:", slice[0])
fmt.Println("最后一个元素:", slice[len(slice)-1])
3.2 追加元素
使用 append
函数追加元素。如果容量不足,会分配新数组并复制数据:
slice := []int{1, 2, 3}
fmt.Println("追加前:", slice, "len:", len(slice), "cap:", cap(slice))
slice = append(slice, 4, 5) // 追加多个元素
fmt.Println("追加后:", slice, "len:", len(slice), "cap:", cap(slice))
输出:
追加前: [1 2 3] len: 3 cap: 3
追加后: [1 2 3 4 5] len: 5 cap: 6 //这里cap: 6,为啥不是5,详见下备注解释
- Go 版本差异:虽然大部分情况下扩容规则遵循翻倍原则,但不同版本的 Go 可能存在细微差异。例如:
- Go 1.18 前:容量小于 1024 时翻倍。
- Go 1.18 及以后:容量小于 256 时翻倍,大于等于 256 时增加 25%。
- 在你的示例中,初始容量为 3,远小于 256,因此应翻倍到 6。
3.3 删除元素
- 删除末尾元素:
slice := []int{1, 2, 3, 4, 5}
slice = slice[:len(slice)-1]
fmt.Println("删除末尾后:", slice)
- 删除指定位置元素:
slice := []int{1, 2, 3, 4, 5}
index := 2
slice = append(slice[:index], slice[index+1:]...)
fmt.Println("删除索引2后:", slice)
3.4 切片复制
使用 copy
函数将一个切片的内容复制到另一个切片:
src := []int{1, 2, 3}
dest := make([]int, len(src))
copy(dest, src)
dest[0] = 99
fmt.Println("原切片未受影响:", src)
fmt.Println("修改后的目标切片:", dest)
输出:
原切片未受影响: [1 2 3]
修改后的目标切片: [99 2 3]
3.5 多维切片
创建多维切片(如二维矩阵):
matrix := make([][]int, 3)
for i := range matrix {
matrix[i] = make([]int, i+1)
for j := 0; j <= i; j++ {
matrix[i][j] = i + j
}
}
fmt.Println("多维切片:", matrix)
输出:
多维切片: [[0] [1 2] [2 3 4]]
4. 切片的共享底层数组
切片是引用类型,多个切片可能共享同一个底层数组。修改共享数组中的元素会影响所有相关切片:
arr := []int{1, 2, 3}
slice1 := arr[:]
slice2 := arr[:]
slice1[0] = 100
fmt.Println("slice1:", slice1)
fmt.Println("slice2:", slice2)
fmt.Println("arr:", arr)
输出:
slice1: [100 2 3]
slice2: [100 2 3]
arr: [100 2 3]
扩容时的深拷贝
当 append
导致容量不足时,会分配新数组,新旧切片不再共享底层数组:
arr := []int{1, 2, 3}
tmp := append(arr[:1], arr[2:]...) // 删除中间元素
fmt.Println("tmp:", tmp, "arr:", arr)
输出:
tmp: [1 3] arr: [1 3 3]
5. 性能优化
- 预分配容量:使用
make([]T, 0, capacity)
避免频繁扩容。 - 避免不必要的复制:通过传递切片指针减少内存开销。
- 合理使用
copy
:当需要独立副本时,使用copy
而非直接赋值。
6. 常见陷阱
6.1 切片的容量和长度
- 长度(
len
):当前元素个数。 - 容量(
cap
):底层数组的总大小。
slice := []int{1, 2, 3}
fmt.Println("len:", len(slice), "cap:", cap(slice)) // len:3, cap:3
6.2 nil
切片与空切片
nil
切片:未初始化的切片。- 空切片:长度为0但容量可能非0。
var s1 []int // nil 切片
s2 := []int{} // 空切片
fmt.Println(s1 == nil) // true
fmt.Println(s2 == nil) // false
在 Go 语言中,判断切片是否为空的正确方法是使用 len(slice) == 0
,而不是直接与 nil
比较(如 slice == nil
)。
为什么不能直接使用 slice == nil
?
Go 语言中,切片是引用类型,其底层由三个字段组成:
array
:指向底层数组的指针。len
:切片的长度。cap
:切片的容量。
切片的 零值 是 nil
,即未初始化的切片。但是,已初始化但长度为 0 的切片(空切片)和 nil
切片是不同的:
var s1 []int // s1 == nil → true,len(s1) == 0 → true
s2 := []int{} // s2 == nil → false,len(s2) == 0 → true
s3 := make([]int, 0) // s3 == nil → false,len(s3) == 0 → true
因此,仅通过 slice == nil
无法覆盖所有空切片的情况。
正确判断切片是否为空的方法
始终使用 len(slice) == 0
来判断切片是否为空,无论其是否是 nil
:
func isEmpty(slice []int) bool {
return len(slice) == 0
}
示例代码
package main
import "fmt"
func main() {
var s1 []int // nil 切片
s2 := []int{} // 空切片
s3 := make([]int, 0) // 空切片
fmt.Println("s1 是否为空:", len(s1) == 0) // true
fmt.Println("s2 是否为空:", len(s2) == 0) // true
fmt.Println("s3 是否为空:", len(s3) == 0) // true
}
常见错误与注意事项
-
错误:直接与
nil
比较if slice == nil { fmt.Println("切片为空") }
问题:上述代码无法检测到非
nil
的空切片(如s2
和s3
)。 -
正确:使用
len(slice) == 0
if len(slice) == 0 { fmt.Println("切片为空") }
优势:无论切片是否是
nil
,只要其长度为 0,均能正确判断。 -
其他场景:检查切片是否为
nil
如果需要区分nil
切片和非nil
的空切片,可以显式比较:if slice == nil { fmt.Println("切片是 nil") } else if len(slice) == 0 { fmt.Println("切片是空的,但不是 nil") }
总结
- 判断切片是否为空的标准方法:
len(slice) == 0
。 - 避免直接比较
slice == nil
,因为非nil
的空切片(如[]int{}
或make([]int, 0)
)同样可能是空的。 - 理解
nil
切片与空切片的区别:nil
切片:未初始化,底层数组不存在。- 空切片:已初始化,底层数组存在但长度为 0。
通过 len(slice) == 0
的判断方式,可以确保所有空切片场景都被正确覆盖。
6.3 扩容策略
- 扩容规则:当容量不足时,Go 会分配新数组,新容量通常是当前容量的两倍(具体实现可能因版本不同而变化)。
s := make([]int, 1, 2)
s = append(s, 2) // 容量足够,不扩容
fmt.Println("len:", len(s), "cap:", cap(s)) // len: 2 cap:2
s = append(s, 3) // 容量不足,扩容
fmt.Println("len:", len(s), "cap:", cap(s)) // len:3, cap:4
7. 总结
切片是Go语言中处理动态数据的核心工具,其灵活性和高效性得益于对底层数组的引用。使用时需注意:
- 共享底层数组的风险:修改共享数组会影响其他切片。
- 预分配容量:减少扩容开销。
- 理解
len
和cap
:正确操作切片长度和容量。 - 避免频繁复制:使用
copy
或重新切片而非直接赋值。
通过合理使用切片,可以编写出高效且易于维护的Go代码。
8.切片和数组的区别
在Go语言中,数组(Array)和切片(Slice)是两种核心的数据结构,但它们在底层实现、使用方式和特性上有显著区别。
一. 长度与灵活性
-
数组:
- 固定长度:数组的长度在声明时确定,且不可更改。例如
var arr [5]int
表示一个长度为5的整型数组。 - 类型的一部分:数组的类型包括其长度,例如
[5]int
和[10]int
是两个不同的类型。 - 无法动态扩容:如果需要更大的数组,必须手动创建新数组并复制数据。
- 固定长度:数组的长度在声明时确定,且不可更改。例如
-
切片:
- 动态长度:切片的长度是动态的,可以通过
append
动态追加元素,甚至扩容。 - 类型不包含长度:切片的类型只包含元素类型(如
[]int
),不依赖长度。 - 自动扩容:当容量不足时,切片会自动分配新数组并复制数据(通常新容量是原容量的2倍)。
- 动态长度:切片的长度是动态的,可以通过
示例:
// 数组:固定长度
var arr [3]int = [3]int{1, 2, 3}
// 切片:动态长度
slice := []int{1, 2, 3}
slice = append(slice, 4) // 动态追加元素
二. 内存结构
-
数组:
- 连续内存块:数组直接存储元素,是一块连续的内存空间。
- 值类型:数组是值类型,赋值或传递时会复制整个数组的数据。
-
切片:
- 结构体封装:切片本质上是一个结构体,包含三个字段:
- 指针:指向底层数组的起始地址。
- 长度(len):当前切片中元素的数量。
- 容量(cap):底层数组的总容量(从切片起始位置到数组末尾的长度)。
- 引用类型:切片是引用类型,传递或赋值时共享底层数组。
- 结构体封装:切片本质上是一个结构体,包含三个字段:
示例:
// 数组的内存结构
arr := [5]int{1, 2, 3, 4, 5} // 直接存储元素
// 切片的内存结构
slice := arr[1:4] // 切片结构体包含指针、长度=3、容量=4
三. 传递方式
-
数组:
- 值传递:将数组传递给函数时,会复制整个数组。对副本的修改不会影响原数组。
- 性能开销:大数组的复制可能带来较高的内存和时间开销。
-
切片:
- 引用传递:切片传递的是结构体的副本(包含指针、长度和容量),因此对切片的修改会直接影响底层数组。
- 高效性:即使切片很大,传递的也只是结构体(指针、长度、容量),开销较小。
示例:
func modifyArray(arr [3]int) {
arr[0] = 100 // 修改的是副本
}
func modifySlice(slice []int) {
slice[0] = 100 // 修改的是底层数组
}
arr := [3]int{1, 2, 3}
modifyArray(arr)
fmt.Println(arr) // 输出 [1 2 3]
slice := []int{1, 2, 3}
modifySlice(slice)
fmt.Println(slice) // 输出 [100 2 3]
四. 扩容机制
-
数组:
- 不可扩容:数组的长度固定,无法动态扩容。如果需要更大的数组,必须手动创建新数组并复制数据。
-
切片:
- 自动扩容:当使用
append
追加元素时,如果切片的容量不足,会分配一个新的底层数组,复制原有数据,并将新元素追加到新数组中。 - 扩容策略:Go 的切片扩容规则通常是将容量翻倍(具体实现可能因版本不同而变化)。
- 自动扩容:当使用
示例:
slice := make([]int, 2, 4) // 长度=2,容量=4
fmt.Println(len(slice), cap(slice)) // 2 4
slice = append(slice, 3) // 容量足够,无需扩容
slice = append(slice, 4) // 容量足够,无需扩容
slice = append(slice, 5) // 容量不足,自动扩容
fmt.Println(len(slice), cap(slice)) // 5 8
五. 使用场景
-
数组:
- 适合固定大小的数据集合:例如矩阵、固定长度的缓存池等。
- 生命周期短的小数据:由于数组是值类型,适合在函数内部临时使用。
-
切片:
- 适合动态集合:例如动态增长的列表、动态数据处理等。
- 需要高效传递和修改数据的场景:切片的引用特性使其更适合函数间传递数据。
六. 空值与判空
-
数组:
- 空数组:数组的零值是元素类型的零值(例如
int
类型的数组初始化为[0, 0, ...]
)。 - 判空:通过
len(array) == 0
判断是否为空数组。
- 空数组:数组的零值是元素类型的零值(例如
-
切片:
- 空切片:切片的零值是
nil
,但也可以通过make([]T, 0)
或[]T{}
创建一个非nil
的空切片。 - 判空:始终通过
len(slice) == 0
判断是否为空切片(因为nil
切片和空切片都可能满足条件)。
- 空切片:切片的零值是
示例:
var arr [0]int // 空数组(长度为0)
var s1 []int // nil 切片
s2 := []int{} // 非 nil 的空切片
fmt.Println(arr == [0]int{}) // true
fmt.Println(s1 == nil) // true
fmt.Println(s2 == nil) // false
fmt.Println(len(s1) == 0) // true
七. 性能与注意事项
-
数组:
- 高性能:由于数组是连续的内存块,访问速度非常快。
- 缺点:灵活性差,无法动态调整大小。
-
切片:
- 灵活但需注意扩容开销:频繁的扩容可能导致性能下降(因为需要复制数据)。
- 共享底层数组的风险:多个切片可能共享同一个底层数组,修改一个切片可能影响其他切片。
示例:
arr := [5]int{1, 2, 3, 4, 5}
s1 := arr[1:3] // [2, 3]
s2 := arr[3:5] // [4, 5]
s1[0] = 100 // 修改底层数组,s2 也会受到影响
fmt.Println(s1) // 输出 [100, 3]
fmt.Println(s2) // 输出 [4, 5]
总结对比表
特性 | 数组 | 切片 |
---|---|---|
长度 | 固定 | 动态 |
类型是否包含长度 | 是 | 否 |
内存结构 | 连续内存块,直接存储元素 | 结构体(指针、长度、容量) |
传递方式 | 值传递(复制整个数组) | 引用传递(共享底层数组) |
扩容能力 | 不可扩容 | 自动扩容 |
适用场景 | 固定大小的数据集合 | 动态集合 |
判空方式 | len(array) == 0 | len(slice) == 0 |
性能 | 访问速度快 | 灵活但需注意扩容开销 |
代码示例
package main
import "fmt"
func main() {
// 数组:固定长度
arr := [3]int{1, 2, 3}
fmt.Println("数组:", arr, "len:", len(arr), "cap:", cap(arr))
// 切片:动态长度
slice := []int{1, 2, 3}
fmt.Println("切片:", slice, "len:", len(slice), "cap:", cap(slice))
// 切片扩容
slice = append(slice, 4, 5)
fmt.Println("扩容后:", slice, "len:", len(slice), "cap:", cap(slice))
// 共享底层数组
arr2 := [5]int{1, 2, 3, 4, 5}
s1 := arr2[1:3]
s2 := arr2[3:5]
s1[0] = 100
fmt.Println("s1 受影响:", s1) // 输出 [100, 5]
fmt.Println("s2 :", s2)
}
结论
- 数组是Go语言中基础的固定长度数据结构,适合对性能要求高且数据大小固定的场景。
- 切片是对数组的封装,提供了动态扩容和更灵活的操作,是处理动态数据集合的首选。
- 在实际开发中,切片的使用频率远高于数组,但需要理解其底层机制以避免性能陷阱(如频繁扩容或共享底层数组导致的副作用)。
- 感谢你赐予我前进的力量