【转载】Go常见错误集锦之混淆slice中的长度(length)和容量(capacity) - Go切片make()初始化相关问题解惑

7 篇文章 0 订阅

本文是本菜鸟在学习go的过程中遇到了切片初始化的问题,搜索了很多之后找到的最好回答,转载至C站。

原文链接:Go常见错误集锦之混淆slice中的长度(length)和容量(capacity) - 知乎

以下是原文(经过排版微调和错别字更正后):


本文是对 《100 Go Mistackes:How to Avoid Them》一书的翻译。因翻译水平有限,难免存在翻译准确性问题,敬请谅解。更多内容请关注公众号–Go学堂

对于Go研发人员来说,对于slice结构中的长度(length)和容量(capacity)经常混淆是很常见的。完全理解这两个概念对有效处理slice的核心操作是至关重要的。例如:对slice的初始化,使用append添加元素,拷贝元素或分隔slice等操作。否则,可能导致使用append操作切片时性能低下,甚至是内存泄露。

在Go语言中,slice的底层实现是数组,也就是说,切片的数据实际上是被存储在数组中的。如果后端的数组空间已经满了或是空数组,则slice结构体负责处理数组容量的扩容或缩容逻辑。

此外,slice的结构体中共拥有三个字段:

  • 一个指针,指向后端的数组,
  • 一个length字段,代表该slice中包含的元素个数。
  • 一个capacity(容量)字段,代表后端数组能够容纳的元素个数。

我们通过两个例子来演示一下slice的结构。

首先,我们使用给定的长度和容量来初始化一个slice:

s := make([]int, 3, 6)  // ①

① 第二个参数3代表长度(length),第三个参数6代表容量(capacity)

如下图所示:

在这里插入图片描述

该切片创建了一个能够容纳6个元素(容量)的数组。同时,因为长度length被设置成了3,所以,Go仅仅初始化前3个元素。因为slice的元素是[]int类型,所以前3个元素用int的零值0来初始化。剩余的元素空间只被分配,但没有使用。

如果打印这个切片,将会得到如下结果:[0 0 0]。

如果我们设置s[1] = 1,那么,该切片的第2个元素将会被更新,但对该slice的长度和容量不会有任何影响。如下图所以:

在这里插入图片描述

但是,不允许访问切片长度(length)以外的元素,即使长度以外的内存空间也已经被分配了。例如,s[4] = 0 会引发panic:

panic:runtime error: index out of range [4] with length 3

那么,我们该如何使用slice中剩余的空间呢?通过内建的append函数:

s = append(s, 2)

该操作将会往s切片中添加一个新的元素。该元素使用第一个图中灰色的元素块(即分配了空间但又没被使用的位置)来存储元素2。如下图所以:

在这里插入图片描述

这时,slice的长度length从3变成了4,即该slice现在有4个元素。

那如果我们再多加入3个元素slice会发生什么?后端的数组空间会不会不足够大了?

s = append(s, 3)
s = append(s, 4)
s = append(s, 5)
fmt.Println(s)

如果我们执行这部分代码,我们会注意到该slice依然能满足我们的需求:

[0 1 0 2 3 4 5]

因为数组是一个固定长度的结构,只能将元素4给存储进去。当我们想插入元素5时,该数组就已经满了,Go会创建另一个数组,并且空间大小是原来容量的2倍,然后将原数组中的所有元素都拷贝到新数组中去,再在新数组中插入元素5,如下图所示:

在这里插入图片描述

现在slice的的指针字段指向了新的数组。那原来的那个数组会怎么样呢?如果没有被引用,将会被GC进行回收。

下面,我们来看看对一个slice进行切分的影响:

s1 := make([]int, 3, 6)  // ①
s2 := s1[1:3]  // ②

① 一个长度为3,容量为6的切片
② 从索引1到3进行切分

如下图:

在这里插入图片描述

首先,s1被初始化成一个长度为3,容量为6的切片。当通过切分s1创建s2切片时,s1和s2的指针字段都指向同一个后端数组。但是,s2的第一个元素的索引是从数组的索引1开始的。因此,切片s2的长度和容量是和s1不同的:长度为2,容量为5.

如果我们更新s1[1]或s2[0],那么对于后端数组来说,变更是一样的。因此,该变更对两个切片都是可见的,如图所示:

在这里插入图片描述

那,如果现在往s2中append一个元素会发生什么呢?会对s1有影响吗?

s2 = append(s2, 2)

这样,会将共享的数组进行修改,但只有s2的长度会发生改变,如图所示:

在这里插入图片描述

s1的长度依然是3,容量是6。因此,如果我们打印s1和s2,那么被加入的元素只对s2可见:

s1 = [0 1 0], s2 = [1 0 2]

在使用append时,理解这个行为会降低出错的概率。

最后一个需要注意的是,如果我们持续往s2中append元素,直到数组满了位置,会发生什么呢? 我们再往s2中增加3个元素,直到将后端的数组填满,没有任何可用的空间:

s2 = append(s2, 3)
s2 = append(s2, 4)
s2 = append(s2, 5)  // ①

① 在该阶段,后端的数组就已经满了。

这段代码会导致创建另一个新的数组,如图所示:

在这里插入图片描述

注意,这时s1和s2分别指向了两个不同的数组。实际上,s1依然是一个长度为3,容量为6的切片,同时也有一些可用的buffer空间,因此,它依然是引用了最初的那个数组。同时,新创建的数组,会从s2的起始位置将数据拷贝到自己的空间上来。这也就是为什么新数组的第一个元素是1,而不是0的原因。

总之,切片中的length是该切片中当前已存储的元素个数,切片的容量是该切片指向的数组的元素个数。往一个满了的切片(切片长度=切片容量)中添加新元素会触发创建一个新的数组,并且新数组的容量是原来的2倍,该新数组会将原数组中的元素都拷贝过来,同时将slice中的指针更新到指向新数组。

  • 5
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值