slice
Slice
在 Go 语言中,slice 是一种非常灵活且功能强大的数据结构,可以看作是对数组的一个轻量级抽象,它本质上是一段连续的内存区域的视图,它包含了底层数组的部分或全部数据,并支持动态扩展
slice 的基本概念
什么是 slice
slice 是对数组的一个抽象视图,它不存储数据,而是指向一个底层数组,与固定大小的数组不同,slice 的长度可以动态改变,这使得它在实际应用中非常灵活
slice 是一种引用类型,多个 slice 可以引用同一个底层数组,对其中一个 slice 进行修改可能会影响到其他引用同一底层数组的 slice
为什么需要 slice
数组在 Go 中是值类型,赋值时会拷贝整个数组,而 slice 只传递三个数据(指针、长度、容量),效率更高,且提供了对数组部分数据的访问能力,可以通过切片表达式方便地获取数组的子序列
内置函数 append、copy 等使得操作 slice 更加便捷
slice 的内部结构
在 Go 语言中,slice 实际上是一个结构体,内部包含三个元素:
-
指针
- 指向底层数组的起始位置,也就是数据存储的内存地址
-
长度(len)
- 表示当前
slice包含了多少个元素,即可访问的元素个数
- 表示当前
-
容量(cap)
-
表示从
slice的起始位置到底层数组的末尾,总共能够包含的元素个数 -
它定义了
slice在不分配新内存的前提下,最多能扩展到的大小
-
可以用下面的伪结构体来表示 slice:
type sliceHeader struct {
Data uintptr // 指向底层数组的指针
Len int // 当前 slice 的长度
Cap int // 当前 slice 的容量
}
这种设计使得 slice 可以作为轻量级的数据视图,同时在扩容时能高效管理内存
slice 的声明与初始化
通过数组切片表达式创建 slice
可以从一个数组中通过下标范围取出一个 slice:
package main
import "fmt"
func main() {
arr := [5]int{10, 20, 30, 40, 50}
// 从数组中取出索引 1 到 3(不包含索引 4)的元素
s := arr[1:4]
fmt.Println("slice s:", s) // 输出 [20 30 40]
fmt.Println("len(s):", len(s))
fmt.Println("cap(s):", cap(s)) // 容量是从 arr[1] 到 arr[len(arr)-1] 的长度,即 4
}
此时,s 引用了 arr 的一部分,修改 s 中的元素也会影响 arr
通过 slice 字面量直接初始化
可以直接使用字面量创建一个 slice,而不必先声明一个数组:
package main
import "fmt"
func main() {
s := []int{1, 2, 3, 4, 5}
fmt.Println("slice s:", s)
fmt.Println("len(s):", len(s))
fmt.Println("cap(s):", cap(s))
}
此时系统会自动创建一个底层数组来存放这些初值
使用 make 创建 slice
使用内置函数 make 可以创建一个具有指定长度和容量的 slice:
package main
import "fmt"
func main() {
// 创建一个长度为 3、容量为 5 的 slice
s := make([]int, 3, 5)
fmt.Println("slice s:", s) // 初始化元素均为 0
fmt.Println("len(s):", len(s)) // 3
fmt.Println("cap(s):", cap(s)) // 5
}
其中 make 函数的第三个参数是可以省略的,也就是初始化时可以只初始化长度
二维数组的初始化
固定大小的二维数组:
// 默认初始化元素是 0
var matrix [3][4]int
// 也可以在声明时指定初始值
matrix := [3][4]int{
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12},
}
动态二维数组(切片):
// 只是创建了一个外层的切片,此时内层每个元素的值默认为 nil,并未分配内存
matrix := make([][]int, n) // n 行
// 要为每一行分配内存,相当于为内层切片赋值
for i := 0; i < n; i++ {
matrix[i] = make([]int, m) // m 列
}
slice 的常见操作
访问元素
与数组类似,可以使用下标访问 slice 中的元素:
s := []string{"Go", "Java", "Python"}
fmt.Println(s[0]) // 输出 "Go"
修改元素
因为 slice 是引用类型,直接通过下标修改可以改变底层数组的值:
s[1] = "C++"
fmt.Println(s) // 结果可能是 ["Go", "C++", "Python"]
追加元素:append
使用内置的 append 函数可以向 slice 添加新元素
当添加的元素超过当前容量时,Go 会自动分配更大的底层数组并将原有数据复制过去:
s := []int{1, 2, 3}
s = append(s, 4, 5)
fmt.Println("slice s:", s) // 输出 [1 2 3 4 5]
复制元素:copy
使用内置的 copy 函数可以从一个 slice 复制数据到另一个 slice,复制的长度为两者中较小的那个:
src := []int{1, 2, 3}
dst := make([]int, 2)
n := copy(dst, src)
fmt.Println("dst:", dst) // 输出 [1, 2]
fmt.Println("n:", n) // 实际复制了 2 个元素
当你对一个 slice 使用 append 操作且超过其当前容量时,运行时会自动分配一个更大的底层数组,并将原有数据复制过去
这种扩容机制会导致原 slice 与新的 slice 指向不同的底层数组,从而可能出现“引用失效”的问题
截取 slice
可以通过切片表达式对 slice 进一步截取:
s := []int{1, 2, 3, 4, 5}
sub := s[2:] // 从索引 2 到结尾
fmt.Println("sub:", sub) // 输出 [3 4 5]
二维数组的遍历和获取元素
遍历:
// 对于切片的切片
for i, row := range matrix {
for j, value := range row {
fmt.Printf("matrix[%d][%d] = %d\n", i, j, value)
}
}
获取元素:
directions := [][]int{{0, 1}, {1, 0}, {0, -1}, {-1, 0}}
// 想要获取内层切片的元素,直接用下标就行
// 从第一个内层切片从取出下标为 0 的元素,也就是 0
value := directions[0][0]
slice 的内存模型和扩容机制
共享底层数组
当多个 slice 引用同一底层数组时,修改其中一个 slice 的内容会影响到其他 slice
例如:
arr := [5]int{10, 20, 30, 40, 50}
s1 := arr[1:4] // 包含 20, 30, 40
s2 := s1[1:3] // 包含 30, 40
s1[1] = 300 // 修改 s1 中的元素
fmt.Println("s2:", s2) // 输出 [300, 40]
这种共享机制既可以节省内存,又需要开发者注意数据的副作用
扩容机制
当使用 append 向 slice 添加元素超过容量时,Go 会自动重新分配内存,并复制原有的数据
新分配的内存大小通常会以指数方式增长,以减少频繁分配内存带来的性能影响
你将 slice 传递给函数时可能只传递了引用,但当内部调用 append 时,如果扩容发生,slice 实际上就指向一个全新的底层数组,这时外部的 slice 不会受到该扩容的影响
内存管理的注意事项
-
尽量避免创建过大的
slice却只使用其中一部分数据,因为底层数组不会自动收缩,这可能会导致内存浪费 -
如果你只需要部分数据,可考虑使用
copy函数将需要的数据拷贝到新的slice中,使得不再使用的部分可以被垃圾回收
slice 与数组的区别
类型定义
数组:例如 [5]int,数组长度是类型的一部分,因此 [5]int 与 [10]int 是不同的类型
slice:例如 []int,与长度无关,是对数组的一种抽象视图
传递方式
数组是值类型,传递时会复制整个数组(除非使用指针)
slice 传递时仅复制结构体(包含指针、长度、容量),因此修改 slice 的元素会反映到底层数组上
灵活性
数组大小固定,不便于动态变化
slice灵活且支持动态扩容,更适合大部分实际场景
总结
slice 是 Go 语言中对数组的一种动态封装, 它由指针、长度和容量组成,可以方便地管理和操作数据集合
通过内置函数 make、切片表达式、append 和 copy, 我们可以非常灵活地创建、扩展和操作 slice
共享底层数组和自动扩容机制提供了高效的内存管理,但也需要开发者注意使用时可能引发的副作用,比如多个 slice 共享同一数据的情况或因扩容而导致引用失效的情形
与数组相比,slice 在大多数实际应用中更加常用和灵活, 因为它不仅避免了数组传值产生的开销,还能够动态调整大小以适应需求