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解析


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

查看所有标签

猜你喜欢:

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

Domain-Driven Design

Domain-Driven Design

Eric Evans / Addison-Wesley Professional / 2003-8-30 / USD 74.99

"Eric Evans has written a fantastic book on how you can make the design of your software match your mental model of the problem domain you are addressing. "His book is very compatible with XP. It is n......一起来看看 《Domain-Driven Design》 这本书的介绍吧!

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具