切片是Go 语言核心的数据结构,然而刚接触 Go 的程序员经常在切片的工作方式和行为表现上被绊倒。比如,明明说切片是引用类型但在函数内对其做的更改有时候却保留不下来,有时候却可以。究其原因是因为我们很多人用其他语言的思维来尝试猜测 Go 语言中切片的行为,切片这个内置类型在 Go 语言底层有其单独的类型定义,而不是我们通常理解的其他语言中数组的概念。
文章翻译自罗伯·派克发布在 Go Blog 中的文章,文中详述了切片是如何被设计出来的以及其与数组的关联和区别,以及内置append函数的实现细节。虽篇幅很长,还是建议认证读完,尤其是关于切片的设计和append函数实现的部分,理解了“切片头”后很多的切片行为就自然而然能够理解。
Rob·Pike
2013 年 9 月 26 日
原文地址: https:// blog.golang.org/slices
介绍
过程编程语言最常见的特征之一就是数组的概念。数组看似简单,但是将数组添加到语言时必须回答许多问题,例如:
- 数组使用固定尺寸还是可变尺寸?
- 尺寸是数组类型的一部分吗?
- 多维数组是什么样的?
- 空数组有意义吗?
这些问题的答案会影响数组是否只是语言的一个普通的功能还是其设计的核心部分。
在 Go 的早期开发中,在感觉到设计正确之前,我们花了大约一年的时间决定对这些问题的答案。非常关键的一步是我们引入了切片,它基于固定大小的数组构建,以提供灵活,可扩展的数据结构。然而,直到今天,刚接触 Go
的程序员经常在切片的工作方式上被绊倒,这也许是因为其他语言的经验固化了他们的思维。
在这篇文章中,我们将尝试消除混乱。我们将通过构建知识片段来解释 append
内置函数的工作原理以及它如此工作的原因。
数组
数组是 Go 中重要的构建块,但就像建筑物的基础一样,它们通常隐藏在可见的组件下。在继续介绍切片的更有趣,更强大和更重要的概念之前,我们必须简短地谈论一下数组。
在 Go 程序中并不经常看到数组,因为数组的大小是数组类型的一部分,这限制了数组的表达能力。
声明数组如下
var buffer [256]byte
声明数组变量 buffer
,其中包含 256 个字节。 buffer
的类型包括其大小,[256] byte
。 一个包含 512 个字节的数组将具有不同的类型 [512] byte
。
与数组关联的数据就是:元素数组。从原理上讲,我们的 buffer
在内存中看起来像这样,
buffer: byte byte byte ... 256 个 ... byte byte byte
也就是说,该变量保存 256 个字节的数据,仅此而已。我们可以通过使用熟悉的索引语法 buffer [0]
,buffer [1]
,buffer [255]
等访问其元素。 (索引范围 0 到 255 涵盖 256 个元素。) 尝试使用该范围之外的值索引数组 buffer
会使程序崩溃。
内置函数 len
的回数组或切片以及其他一些数据类型的元素数量。对于数组,很明显 len
会返回什么。在我们的示例中,len(buffer)
返回固定值 256。
数组有自己的一席之地 (例如,它们很好地表示了转换矩阵),但是它们在 Go 中最常见的应用目的是保留切片的存储空间。
Slices:切片头
切片是执行操作的地方,但是要充分利用它们,开发者必须准确了解它们的含义和作用。
切片是一种数据结构,描述与切片变量本身分开存储的数组的一段连续的部分,。 切片不是数组。切片描述一块数组。
用上节给定的数组变量 buffer,我们可以创建一个描述了数组第 100 个元素到第 150 个元素的切片(准确地说是包含第 100 个元素到 149 个元素):
var slice []byte = buffer[100:150]
在该代码段中,我们使用了完整的变量声明。变量slice
的类型为 [] byte
的 “字节切片”,并通过从名为 buffer
的数组切片第 100 个元素 (包括) 到第 150 个元素 (不包括) 来初始化。更惯用的语法是忽略类型,类型由初始化表达式设置:
var slice = buffer[100:150]
在函数内部,我们可以使用简短声明形式,
slice := buffer[100:150]
切片变量到底是什么?现在将 slice 看作是一个具有两个元素的小数据结构:长度和指向数组元素的指针。你可以认为它是在底层像这样被构建的:
type sliceHeader struct {
Length int
ZerothElement *byte
}
slice := sliceHeader{
Length: 50,
ZerothElement: &buffer[100],
}
当然,这只是一个为了说明举的例子。尽管此代码段说明了 sliceHeader
结构对于程序员是不可见的,并且元素指针的类型取决于元素的类型,但这给出了切片机制大体上的概念。
到目前为止,我们已经对数组使用了切片操作,但是我们也可以对切片进行切片操作,如下所示:
slice2 := slice[5:10]
和之前一样,此操作将创建一个新的切片,在这种情况下,新切片将使用原始切片的元素 5 至 9,也就是原始数组的元素 105 至 109。 slice2
变量底层的 sliceHeader
结构如下所示:
slice2 := sliceHeader{
Length: 5,
ZerothElement: &buffer[105],
}
请注意,此标头仍指向存储在 buffer
变量中的相同底层数组。
我们还可以重切片,也就是说对切片进行切片操作,然后将结果存储回原始切片结构中。在执行下面的切片操作后
slice = slice[5:10]
slice
变量的 sliceHeader
结构看起来和 slice2
变量的结构一样。在使用Go
的过程中你将会看到重切片会被经常使用,例如截断切片。下面的语句删除切片的第一个和最后一个元素: