go语言梳理-slice切片解析

栏目: Go · 发布时间: 5年前

内容简介:切片是一种数据结构,这种数据结构便于使用和管理数据集合。切片是围绕动态数组的概念构建的,可以按需自动增长和缩小。切片的动态增长是通过内置函数 append 来实现的。这个函数可以快速且高效地增长切片。还可以通过对切片再次切片来缩小一个切片的大小。因为切片的底层内存也是在连续块中分配的,所以切片还能获得索引、迭代以及为垃圾回收优化的好处。Go 语言中有几种方法可以创建和初始化切片。是否能提前知道切片需要的容量通常会决定要如何创建切片。

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

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

内部实现

切片是一个很小的对象,对底层数组进行了抽象,并提供相关的操作方法。切片有 3 个字段的数据结构,这 3 个字段分别是指向底层数组的指针、切片访问的元素的个数(即长度)和切片允许增长到的元素个数(即容量)。这些数据结构包含 Go 语言需要操作底层数组的元数据.如下图

go语言梳理-slice切片解析

创建和初始化

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

  • 1.0 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 中的切片动作后,我们有了两个切片,它们共享同一段底层数组,但通过不同的切片会看到底层数组的不同部分, 如下图:

    go语言梳理-slice切片解析

    第一个切片 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 操作完成后,两个切片和底层数组的布局如图

    go语言梳理-slice切片解析

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

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

    // 创建一个整型切片
    
    // 其长度和容量都是 4 个元素
    
    slice := []int{10, 20, 30, 40}
    
    // 向切片追加一个新元素
    
    // 将新元素赋值为 50
    
    newSlice := append(slice, 50)
    

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

    go语言梳理-slice切片解析

    函数 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 元素,如下图

    go语言梳理-slice切片解析

    三个索引计算长度和容量

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

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

    go语言梳理-slice切片解析

转载自: slice解析


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

你必须知道的495个C语言问题

你必须知道的495个C语言问题

Steve Summit / 孙云、朱群英 / 人民邮电出版社 / 2009-2 / 45.00元

“本书是Summit以及C FAQ在线列表的许多参与者多年心血的结晶,是C语言界最为珍贵的财富之一。我向所有C语言程序员推荐本书。” ——Francis Glassborow,著名C/C++专家,ACCU(C/C++用户协会)前主席 “本书清晰阐明了Kernighan与Ritchie《The C programming Language》一书中许多简略的地方,而且精彩地总结了C语言编程......一起来看看 《你必须知道的495个C语言问题》 这本书的介绍吧!

HTML 压缩/解压工具
HTML 压缩/解压工具

在线压缩/解压 HTML 代码

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器