本文是对 Go 官方博客文章 Go Slices: usage and internals 的翻译与总结。
简介
Go 语言中的切片(Slice)提供了一种方便且高效的方式来处理类型化数据序列。切片类似于其他语言中的数组,但具有一些独特的属性。
数组 (Arrays)
切片类型是构建在 Go 数组类型之上的抽象,因此要理解切片,我们必须先理解数组。
- 固定大小:数组类型定义指定了长度和元素类型(例如
[4]int)。数组的大小是固定的,长度是其类型的一部分。 - 无需显式初始化:数组的零值是一个随时可用的数组,其元素本身被置零。
- 值类型:Go 的数组是值类型。数组变量表示整个数组,而不是指向第一个元素的指针。当赋值或传递数组时,会复制其完整内容。
切片 (Slices)
切片建立在数组之上,提供了强大的功能和灵活性。
- 无固定长度:切片的类型规范是
[]T,没有指定的长度。 - 创建方式:可以使用内置函数
make([]T, len, cap)创建切片。如果省略容量(cap),它默认为指定的长度。 - 零值:切片的零值是
nil。对于 nil 切片,len和cap均为 0。 - 切片操作:可以通过指定半开区间(如
b[1:4])对现有的切片或数组进行“切片”。
切片的内部原理
切片本质上是数组片段的描述符。 它在底层由三个部分组成:
- 指针 (Pointer):指向底层数组中切片起始元素的指针。
- 长度 (Length):切片当前引用的元素个数(可通过
len()获取)。 - 容量 (Capacity):底层数组中从切片起始元素到底层数组末尾的元素个数(可通过
cap()获取)。
注意:对切片进行切片操作(slicing)不会复制切片的数据,而是创建一个指向原始底层数组的新切片值。因此,修改新切片的元素会直接修改原始切片(及底层数组)的元素。切片不能增长超过其容量,否则会导致运行时 panic。
增长切片 (copy 和 append)
当切片需要超出其容量时,必须分配一个新的、更大的底层数组,并将原始数据复制过去。
copy函数:copy(dst, src []T) int用于将数据从源切片复制到目标切片。append函数:append(s []T, x ...T) []T用于向切片末尾追加元素。如果底层数组容量不足,append会自动分配一个更大的数组,复制原有数据,并返回更新后的切片。
a := []string{"John", "Paul"}
b := []string{"George", "Ringo", "Pete"}
a = append(a, b...) // 追加另一个切片的所有元素常见的内存泄漏“陷阱”
由于对切片进行切片操作不会复制底层数组,整个底层数组将保留在内存中,直到没有任何切片引用它。
场景:如果你将一个大文件读取到内存中(得到一个大字节切片),然后通过切片操作只返回其中很小的一段匹配数据。由于返回的小切片仍然引用着原始的大数组,垃圾回收器(GC)无法释放整个大文件的内存。
解决方法:在返回之前,将需要保留的小段数据 copy 或 append 到一个新的切片中,从而解除对原始大数组的引用。
func CopyDigits(filename string) []byte {
b, _ := ioutil.ReadFile(filename)
b = digitRegexp.Find(b)
// 复制到新的切片中,避免原始大数组 b 驻留内存
c := make([]byte, len(b))
copy(c, b)
return c
}