一尘不染

切片中指针的行为

go

从切片创建切片的行为是什么?当您像这样定义切片时:

s := []int{2, 3, 5, 7, 11, 13}

您想要这样修改切片:

s = s[:3]
// s = [2 3 5]
s = s[:cap(s)]
// s = [2 3 5 7 11 13]

它实际上可以“扩展”您的切片。哪个无效:

s = s[2:]
// s = [5 7 11 13]
s = [:cap(s)]
// s = [5 7 11 13]

因此,在这种情况下,当您创建新的切片时,您将无法“保留”前两个元素。即使基础数组未更改,也无法更改指向该数组开头的指针,对吗?这是为什么?


阅读 235

收藏
2020-07-02

共1个答案

一尘不染

正如@JimB在注释中指出的,这是由于slice在Go中的功能。

基本上,切片标头是一个包含3个元素的结构:一个指向第一个元素的指针,当前数据的长度以及 根据“第一个元素”指针测量
的基础数组的总容量(即使它只是一部分)实际分配的基础数组的大小)。它在基础数组上没有其他信息。

当通过从端部切断数据s[:x],要创建具有不同长度字段的新切片标头,但 同样的
容量和第一元素的指针。因此,您可以再次将切片扩展到最大容量,因为运行时知道该内存已分配并且可以安全访问。

当使用时s[x:],您将创建一个新的slice头,它具有指向新的第一个元素的不同初始指针,并具有减小的容量以及减小的长度。您不能“撤消”该操作以使其指向原始的第一个元素,因为切片结构除了指针和容量字段外,没有有关原始切片或基础数组的信息。

例如,如果您制作slice s := []int{2, 3, 5, 7, 11, 13},则slice头可能包含:

ptr: 0x00000000
len: 6
cap: 6

如果您随后调用s = s[:3],则切片头将包含:

ptr: 0x00000000
len: 3
cap: 6

注意只有len改变了。如果您随后调用s = s[:cap(s)],则运行时将看到该容量可以支持该片操作,这意味着基础数组至少分配了那么多的“插槽”,并且可以毫无问题地扩展该片。由于原始数据仍位于这些“插槽”中,因此您将获得与原始数组在功能上等效的内容:

ptr: 0x00000000
len: 6
cap: 6

但是,如果您致电s = s[2:],则会得到以下信息:

ptr: 0x00000010
len: 4
cap: 4

请注意,除了更改长度之外,指针还增加了16个字节(在int64位系统上为2 s
的大小),并且由于下面的数组仅分配了4个“插槽”,所以容量字段也已减小。相对于该指针。

现在,运行时 除了该标头之外没有关于基础指针的其他信息! 从该标头中,运行时 无法
知道原始基础数组的大小或原始起点的位置,甚至无法知道当前切片不是从该原始起点开始的。结果,尝试将片重置回原始起点是不合法的,因为运行时无法验证内存是否已分配且安全。这是一个有意的设计决策,因为Go专门设计为不允许使用C
++程序中常见的不良建议和高度笨拙的任意指针算法。

此外,您的呼叫s = s[:cap(s)]使用的是切片头中存储的 (减少的)容量。虽然您第一次进行此调用等效于s = s[:6],因为这是原始切片的容量,但现在调用等效于s = s[:4],因为移动切片的指针还减少了后备数组的容量(因为该容量是从所指向的元素测量的)指向该指针, 而不是 实际后备数组的第一个元素)。

如果Go运行时改为将切片作为指针(指向后备数组中的绝对第一个元素),长度,容量和 offset进行
跟踪,则可以实现所需的操作。但是,它不会这样做。部分原因是因为这将导致切片标头的大小增加33%(专门用于尽可能轻量级),部分原因是正如@JimB所指出的那样,该语言的开发人员决定增加额外的复杂性不必要,因为如果您认为有必要,可以轻松地自己处理原始切片标头。

2020-07-02