slice解析

cdwei · · 1370 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

切片

切片是一种数据结构,这种数据结构便于使用和管理数据集合。切片是围绕动态数组的概念构建的,可以按需自动增长和缩小。切片的动态增长是通过内置函数 append 来实现的。

这个函数可以快速且高效地增长切片。还可以通过对切片再次切片来缩小一个切片的大小。因为切片的底层内存也是在连续块中分配的,所以切片还能获得索引、迭代以及为垃圾回收优化的好处。

 

内部实现

切片是一个很小的对象,对底层数组进行了抽象,并提供相关的操作方法。切片有 3 个字段的数据结构,这些数据结构包含 Go 语言需要操作底层数组的元数据.如下图

这 3 个字段分别是指向底层数组的指针、切片访问的元素的个数(即长度)和切片允许增长到的元素个数(即容量)。

创建和初始化

Go 语言中有几种方法可以创建和初始化切片。是否能提前知道切片需要的容量通常会决定要如何创建切片。

1.make和切片字面量

一种创建切片的方法是使用内置的 make 函数。当使用 make 时,需要传入一个参数,指定切片的长度

1.1使用长度声明一个字符串切片

// 创建一个字符串切片

// 其长度和容量都是 5 个元素

slice := make([]string, 5)

如果只指定长度,那么切片的容量和长度相等。也可以分别指定长度和容量,如1.2

1.2 使用长度和容量声明整型切片

// 创建一个整型切片

// 其长度为 3 个元素,容量为 5 个元素

slice := make([]int, 3, 5)

分别指定长度和容量时,创建的切片,底层数组的长度是指定的容量,但是初始化后并不能访问所有的数组元素。例如1.2创建的切片可以访问 3 个元素,

而底层数组拥有 5 个元素。剩余的 2 个元素可以在后期操作中合并到切片,可以通过切片访问这些元素。

1.3 通过切片字面量来声明切片

// 创建字符串切片

// 其长度和容量都是 5 个元素

slice := []string{"Red", "Blue", "Green", "Yellow", "Pink"}

// 创建一个整型切片

// 其长度和容量都是 3 个元素

slice := []int{10, 20, 30}

当使用切片字面量时,可以设置初始长度和容量。要做的就是在初始化时给出所需的长度和容量作为索引。如1.4创建长度和容量都是100 个元素的切片。

1.4 使用索引声明切片

// 创建字符串切片

// 使用空字符串初始化第 100 个元素

slice := []string{99: ""}

 

使用切片

2.1 使用切片字面量来声明切片

// 创建一个整型切片

// 其长度和容量都是 5 个元素

slice := []int{10, 20, 30, 40, 50}

// 创建一个新切片

// 其长度为 2 个元素,容量为 4 个元素

newSlice := slice[1:3]

两个索引计算长度和容量

对底层数组容量是 k 的切片 slice[i:j]来说

长度: j - i

容量: k - i

对底层数组容量是 5 的切片 slice[1:3]来说

长度: 3 - 1 = 2

容量: 5 - 1 = 4

执行完代码2.1 中的切片动作后,我们有了两个切片,它们共享同一段底层数组,但通过不同的切片会看到底层数组的不同部分, 如下图

第一个切片 slice 能够看到底层数组全部 5 个元素的容量,不过之后的 newSlice 就看不到。对于 newSlice,底层数组的容量只有 4 个元素。newSlice 无法访问到它所指向的底层数组的第一个元素之前的部分。

所以,对 newSlice 来说,之前的那些元素就是不存在的。现在两个切片共享同一个底层数组。如果一个切片修改了该底层数组的共享部分,另一个切片也能感知到, 如下

// 创建一个整型切片

// 其长度和容量都是 5 个元素

slice := []int{10, 20, 30, 40, 50}

创建一个新切片

其长度是 2 个元素,容量是 4 个元素

newSlice := slice[1:3]

// 修改 newSlice 索引为 1 的元素

// 同时也修改了原来的 slice 的索引为 2 的元素

newSlice[1] = 35

把 35 赋值给 newSlice 的第二个元素(索引为 1 的元素)的同时也是在修改原来的 slice的第 3 个元素(索引为 2 的元素)

slice变为[]int{10, 20, 35, 40, 50},  newSlice变为[]int{20, 35}

2.2 切片append扩容

// 其长度和容量都是 5 个元素

slice := []int{10, 20, 30, 40, 50}

// 创建一个新切片

// 其长度为 2 个元素,容量为 4 个元素

newSlice := slice[1:3]

// 使用原有的容量来分配一个新元素

// 将新元素赋值为 60

newSlice = append(newSlice, 60)

append 操作完成后,两个切片和底层数组的布局如图

因为 newSlice 在底层数组里还有额外的容量可用,append 操作将可用的元素合并到切片的长度,并对其进行赋值。由于和原始的 slice 共享同一个底层数组,slice 中索引为3的元素的值也被改动了。

如果切片的底层数组没有足够的可用容量,append 函数会创建一个新的底层数组,将被引用的现有的值复制到新数组里,再追加新值, 如下

// 创建一个整型切片

// 其长度和容量都是 4 个元素

slice := []int{10, 20, 30, 40}

// 向切片追加一个新元素

// 将新元素赋值为 50

newSlice := append(slice, 50)

当这个 append 操作完成后,newSlice 拥有一个全新的底层数组,这个数组的容量是原来的两倍, 如下图

函数 append 会智能地处理底层数组的容量增长。在切片的容量小于 1000 个元素时,总是会成倍地增加容量。一旦元素个数超过1000,容量的增长因子会设为 1.25,也就是会每次增加 25%的容量。

随着语言的演化,这种增长算法可能会有所改变。

2.3 使用 3 个索引创建切片

// 创建字符串切片
// 其长度和容量都是 5 个元素
source := []string{"Apple", "Orange", "Plum", "Banana", "Grape"}

// 将第三个元素切片,并限制容量

// 其长度为 1 个元素,容量为 2 个元素

slice := source[2:3:4]

这个切片操作执行后,新切片里从底层数组引用了 1 个元素,容量是 2 个元素。具体来说,新切片引用了 Plum 元素,并将容量扩展到 Banana 元素,如下图

三个索引计算长度和容量

对于 slice[i:j:k] 或 [2:3:4]

长度: j – i 或 3 - 2 = 1

容量: k – i 或 4 - 2 = 2

和之前一样,第一个值表示新切片开始的元素的索引位置,这个例子中是 2。第二个值表示开始的索引位置(2)加上希望包括的元素的个数(1),

2+1 的结果是 3,所以第二个值就是 3。为了设置容量,从索引位置 2 开始,加上希望容量中包含的元素的个数(2),就得到了第三个值 4。

设置容量大于已有容量的语言运行时错误

如果试图设置的容量比可用的容量还大,就会得到一个语言运行时错误,如下

// 这个切片操作试图设置容量为 4 
// 这比可用的容量大
slice := source[2:3:6]
Runtime Error:
panic: runtime error: slice bounds out of range

因为内置函数 append 会首先使用可用容量。一旦没有可用容量,会分配一个新的底层数组。这导致很容易忘记切片间正在共享同一个底层数组。

一旦发生这种情况,对切片进行修改,很可能会导致随机且奇怪的问题。对切片内容的修改会影响多个切片,却很难找到问题的原因。

设置长度和容量一样的好处

如果在创建切片时设置切片的容量和长度一样,就可以强制让新切片的第一个 append 操作创建新的底层数组,与原有的底层数组分离。

新切片与原有的底层数组分离后,可以安全地进行后续修改,如下

// 创建字符串切片

// 其长度和容量都是 5 个元素

source := []string{"Apple", "Orange", "Plum", "Banana", "Grape"}

// 对第三个元素做切片,并限制容量

// 其长度和容量都是 1 个元素

slice := source[2:3:3]

// 向 slice 追加新字符串

slice = append(slice, "Kiwi")

如果不加第三个索引,由于剩余的所有容量都属于 slice,向 slice 追加 Kiwi 会改变原有底层数组索引为 3 的元素的值 Banana。不过在代码中我们限制了 slice 的容量为 1。

当我们第一次对 slice 调用 append 的时候,会创建一个新的底层数组,这个数组包括 2 个元素,并将水果 Plum 复制进来,再追加新水果 Kiwi,并返回一个引用了这个底层数组的新切片

因为新的切片 slice 拥有了自己的底层数组,所以杜绝了可能发生的问题。我们可以继向新切片里追加水果,而不用担心会不小心修改了其他切片里的水果。同时,也保持了为切片申请新的底层数组的简洁。

上面代码操作之后的新切片的表示如下图

Reference:《Go语言实战》


有疑问加站长微信联系(非本文作者)

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

1370 次点击  
加入收藏 微博
2 回复  |  直到 2019-05-20 17:01:04
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传