0%

go语言梳理-slice切片解析

切片

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

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

内部实现

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

创建和初始化

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

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

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

    1
    2
    3
    4
    5
    // 创建一个字符串切片

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

    slice := make([]string, 5)

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

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

    1
    2
    3
    4
    5
    // 创建一个整型切片

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

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

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

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

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 创建字符串切片

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

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

    // 创建一个整型切片

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

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

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

  • 1.4 使用索引声明切片

    1
    2
    3
    4
    5
    // 创建字符串切片

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

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

使用切片

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 创建一个整型切片

    // 其长度和容量都是 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 来说,之前的那些元素就是不存在的。现在两个切片共享同一个底层数组。如果一个切片修改了该底层数组的共享部分,另一个切片也能感知到, 如下

    1
    2
    3
    4
    5
    // 创建一个整型切片

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

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

    创建一个新切片

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

    1
    2
    3
    4
    5
    6
    7
    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扩容

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // 其长度和容量都是 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 函数会创建一个新的底层数组,将被引用的现有的值复制到新数组里,再追加新值, 如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 创建一个整型切片

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

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

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

    // 将新元素赋值为 50

    newSlice := append(slice, 50)

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

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

  • 2.3 使用 3 个索引创建切片

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 创建字符串切片
    // 其长度和容量都是 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。

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

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

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

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

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

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

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

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // 创建字符串切片

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

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

转载自:slice解析