slice 的底层数据是数组,slice 是对数组的封装,它描述一个数组的片段。两者都可以通过下标来访问单个元素。

    数组是定长的,长度定义好之后,不能再更改。在 Go 中,数组是不常见的,因为其长度是类型的一部分,限制了它的表达能力,比如 [3]int 和 [4]int 就是不同的类型。

    而切片则非常灵活,它可以动态地扩容。切片的类型和长度无关。

    数组就是一片连续的内存, slice 实际上是一个结构体,包含三个字段:长度、容量、底层数组。

    1. // runtime/slice.go
    2. type slice struct {
    3. array unsafe.Pointer // 元素指针
    4. len int // 长度
    5. cap int // 容量
    6. }

    slice 的数据结构如下:

    切片数据结构

    注意,底层数组是可以被多个 slice 同时指向的,因此对一个 slice 的元素进行操作是有可能影响到其他 slice 的。

    【引申1】[3]int 和 [4]int 是同一个类型吗?

    不是。因为数组的长度是类型的一部分,这是与 slice 不同的一点。

    【引申2】下面的代码输出是什么?

    说明:例子来自雨痕大佬《Go学习笔记》第四版,P43页。这里我会进行扩展,并会作图详细分析。

    1. package main
    2. import "fmt"
    3. func main() {
    4. slice := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
    5. s1 := slice[2:5]
    6. s2 := s1[2:6:7]
    7. s2 = append(s2, 100)
    8. s2 = append(s2, 200)
    9. s1[2] = 20
    10. fmt.Println(s1)
    11. fmt.Println(s2)
    12. fmt.Println(slice)
    13. }

    结果:

    1. [2 3 20]
    2. [4 5 6 7 100 200]
    3. [0 1 2 3 20 5 6 7 100 9]

    s1slice 索引2(闭区间)到索引5(开区间,元素真正取到索引4),长度为3,容量默认到数组结尾,为8。s2s1 的索引2(闭区间)到索引6(开区间,元素真正取到索引5),容量到索引7(开区间,真正到索引6),为5。

    slice origin

    接着,向 s2 尾部追加一个元素 100:

    1. s2 = append(s2, 100)

    s2 容量刚好够,直接追加。不过,这会修改原始数组对应位置的元素。这一改动,数组和 s1 都可以看得到。

    append 100

    再次向 s2 追加元素200:

    1. s2 = append(s2, 100)

    这时,s2 的容量不够用,该扩容了。于是,s2 另起炉灶,将原来的元素复制新的位置,扩大自己的容量。并且为了应对未来可能的 append 带来的再一次扩容,s2 会在此次扩容的时候多留一些 buffer,将新的容量将扩大为原始容量的2倍,也就是10了。

    append 200

    最后,修改 s1 索引为2位置的元素:

    1. s1[2] = 20

    这次只会影响原始数组相应位置的元素。它影响不到 s2 了,人家已经远走高飞了。

    s1[2]=20

    再提一点,打印 s1 的时候,只会打印出 s1 长度以内的元素。所以,只会打印出3个元素,虽然它的底层数组不止3个元素。