• 【Go语言】Go语言中的切片


    Go语言中的切片

    1.切片的定义

    Go语言中,切片是一个新的数据类型数据类型,与数组最大的区别在于,切片的类型中只有数据元素的类型,而没有长度:

    var slice []string = []string{"a", "b", "c"}

    因此,Go语言中的切片是一个可变长度的、同一类型元素集合,切片的长度可以随着元素数量的增长而增长,但不会随着元素数量的减少而减少,但切片底层依然使用数组来管理元素,可以看作是对数组做了一层简单的封装。

    创建切片的方法共有三种,分别是基于数组、切片和直接创建。

    1.1 基于数组创建切片

    切片可以基于一个已存在的数组创建,切片可以只使用数组的一部分元素或者全部元素,甚至可以创建一个比数组更大的切片。

    1. // 先定义一个数组
    2. months := [...]string{"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"}
    3. // 基于数组创建切片
    4. q2 := months[3:6] // 第二季度
    5. summer := months[5:8] // 夏季

    Go语言支持通过 array[start:end]这样的方式基于数组生成一个切片,start表示切片在数组中的下标七点,end表示切片在数组中的下表终点,两者之间的元素就是切片初始化后的元素集合,以下是几种创建切片的示例:

    • 基于months 的所有元素创建切片(全年)

      all := months[:]
    • 基于 months 的前6个元素创建切片(上半年)

      firsthalf := months[:6]
    • 基于第6个元素开始的后的后续元素创建切片(下半年)

      secondhalf := months[6:]

    1.2 基于切片创建切片

    类似于切片能够基于一个数组创建,切片也能够基于另一个切片创建:

    1. firsthalf := months[:6]
    2. q1 := firsthalf[:3] // 基于firsthalf的前三个元素构建新切片

    基于切片创建切片时,选择的元素范围可以超过所包含元素的个数,如下:

    1. // 基于切片创建切片
    2. firsthalf := months[:6]
    3. q1 := firsthalf[:3]
    4. // 可以创建超过切片的元素
    5. q3 := q1[:12]

    如上图所示,q3长度远超过q1的长度,超出的部分由原数组months中的元素进行补充,那能不能超过这个原数组的长度呢?

    产生了报错,显示切片的长度为13,但是容量是12,因此这里虽然是基于切片创建切片,但其本质依旧是基于数组创建切片。

    1.3 直接创建切片

    创建切片并不是一定需要一个数组,Go语言的内置函数make()可以灵活地创建切片。

    创建一个初始长度位5的整型切片:

    mySlice := make([]int, 5)

    创建一个初始长度为5,容量为10的整型切片:

    mySlice2 := make([]int, 5, 10)

    创建并初始化包含5个元素的数组切片(长度和容量均为5):

    1. // 这个语句容易和数组的初始化语句混淆
    2. // 数组的初始化语句 array := [5]int{1,2,3,4,5}
    3. // 这两个的区别在于切片初始化不需要指定切片长度,而数组需要指定数组长度
    4. mySlice3 := []int{1, 2, 3, 4, 5}

    和数组类型一样,所有未初始化的切片,会填充元素类型对应的零值。

    实际上,使用直接方式创建切片时,Go底层还是会有一个匿名数组被创建出来,然后调用基于数组创建切片的方式返回切片,只是上层并不需要关心这个匿名数组的操作。因此,最终切片都是基于数组创建的,切片可以看作是操作数组的指针。

    2 切片的遍历

    前面提到,切片可以看作是数组指针,因此操作数组元素的所有方法也适用于切片,例如切片也能够使用下标获取元素,使用len()函数获取元素个数,并支持使用range关键字来快速遍历所有的元素。

    传统的数组遍历方法:

    1. for i := 0; i < len(summer); i++ {
    2. fmt.Println("summer[", i, "] =", summer[i])
    3. }

    也可以使用range关键字遍历:

    1. for i, v := range summer {
    2. fmt.Println("summer[", i, "] =", v)
    3. }

    3 动态增加元素

    切片与数组相比,优势在于支持动态增加元素,甚至能够在容量不足的情况,在切片类型中,元素个数和实际可分配的存储空间是两个不同的值,元素的个数即切片的实际长度,而可分配的存储空间就是切片的容量。

    一个切片的容量初始值根据创建方式有以下两种情况:

    • 对于基于数组和切片创建的切片而言,默认的容量是从切片起始索引到对应底层数组的结尾索引。

    • 对于通过内置make函数创建的切片而言,在没有指定容量参数的情况下,默认容量和切片长度一致。

    因此,通常情况下一个切片的长度值小于等于其容量值,能够通过Go语言内置的cap()函数和len()函数来获取某个切片的容量和实际长度:

    1. var oldSlice = make([]int, 5, 10)
    2. fmt.Println("len(oldSlice):", len(oldSlice))
    3. fmt.Println("cap(oldSlice):", cap(oldSlice))

    此时,切片 oldSilece 的默认值是 [0,0,0,0,0],可以通过append()函数向切片追加新元素:

    newSlice := append(oldSlice, 1, 2, 3)

    append() 函数的第二个参数是一个不定参数,可以根据自己的需求添加元素(大于等于1个),也可以直接将一个切片追加到另一个切片的末尾:

    1. slice2 := []int{1, 2, 3, 4, 5}
    2. // 注意append()后面的...不能省略
    3. slice3 := append(newSlice, slice2...)

    4 自动扩容

    如果追加的元素个数超出切片的默认容量,则底层会自动进行扩容:

    1. oldSlice := []int{1, 2, 3, 4, 5}
    2. newSlice := append(oldSlice, 6, 7, 8, 9)
    3. fmt.Println("oldSlice:", oldSlice, "len:", len(oldSlice), "cap:", cap(oldSlice))
    4. fmt.Println("newSlice:", newSlice, "len:", len(newSlice), "cap:", cap(newSlice))

    此时,newSlice 的长度变成了9,容量变成了10,需要注意的是 append() 函数并不会改变原来的切片,而是会生成一个容量更大的切片,然后把原有的元素和新元素一并拷贝到新切片中。

    默认情况下,扩容后的新切片容量将会是原切片容量的两倍,如果还不能够容纳新元素,则按照同样的操作继续扩容,直到新切片的容量不小于原长度与要追加的元素之和。但是,当原切片的长度大于或等于1024时,Go语言会以原容量的1.25倍作为新容量的基准。

    在编码中,如果能够事先预估切片的容量并在初始化时合理地设置容量值,可以大幅降低切片内部重新分配内存和搬送内存块的操作次数,从而提升程序性能。

    5 内容复制

    Go语言提供了内置函数copy(),用于将元素从一个切片复制到另一个切片,如果两个切片不一样大,就会按照其中较小的那个切片元素个数进行复制。

    1. slice1 := []int{1, 2, 3, 4, 5}
    2. slice2 := []int{6, 7, 8}
    3. // 复制slice1到slice2,复制slice1的前三个元素到slice2中
    4. copy(slice2, slice1)
    5. fmt.Println("slice1:", slice1, "len:", len(slice1), "cap:", cap(slice1))
    6. fmt.Println("slice2:", slice2, "len:", len(slice2), "cap:", cap(slice2))
    7. slice3 := []int{1, 2, 3, 4, 5}
    8. slice4 := []int{6, 7, 8}
    9. fmt.Println("复制slice4到slice3")
    10. // 复制slice4到slice3,复制slice4的所有元素到slice3的前三个元素
    11. copy(slice3, slice4)

    6 动态删除元素

    切片除了支持动态增加元素之外,还可以动态删除元素,在切片中动态删除元素可以通过多种方式实现(底层是通过切片的切片实现):

    1. slice1 := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
    2. slice1 = slice1[:len(slice1)-5] // 删除 slice1 尾部 5 个元素
    3. slice2 := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
    4. slice2 = slice2[5:] // 删除 slice2头部 5 个元素

    还能够通过 append 实现切片元素的删除:

    1. slice3 := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
    2. slice4 := append(slice3[:0], slice3[3:]...) // 删除开头三个元素

    注意append方法的使用, 如 slice4 := append(slice3[:0], slice3[3:]...) 这种方式:

    • slice3[:0] 创建了一个长度为 0 的切片,但底层数组仍然是 slice3 的底层数组。

    • slice3[3:] 创建了一个包含 slice3 从索引3开始的所有元素的切片。

    append 将第一个切片的元素追加到第二个切片中,因此 slice4 包含 slice3 从索引3开始的所有元素。

    这里的问题在于,由于slice4最初共享底层数组,对 slice4 的修改实际上也会影响到 slice3,从而导致 slice3 切片也发生了变化。

    如果 slice4 由两个切片拼接,也会出现类似的问题,例如:

    1. slice5 := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
    2. slice6 := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
    3. slice7 := append(slice5[:3], slice6[6:]...)

    如果想要保证两个切片是完全独立的,不共享底层数组,可以使用copy函数来进行切片的删除。

    使用 copy 函数进行元素的删除:

    1. slice8 := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
    2. slice9 := make([]int, len(slice3)-3)
    3. copy(slice9, slice3[3:]) // 删除开头前三个元素

    7 数据共享问题

    切片底层是基于数组实现的,对应的结构体对象如下所示:

    1. type slice struct {
    2. array unsafe.Pointer //指向存放数据的数组指针
    3. len int //长度有多大
    4. cap int //容量有多大
    5. }

    在结构体中使用指针存在不同实例的数据共享问题,示例代码如下:

    1. slice1 := []int{1, 2, 3, 4, 5}
    2. slice2 := slice1[1:3]
    3. slice2[1] = 6
    4. fmt.Println("slice1:", slice1)
    5. fmt.Println("slice2:", slice2)

    slice2 是基于 slice1 创建的,它们的数组指针指向了同一个数组,因此,修改 slice2 元素会同步到 slice1,因为修改的是同一份内存数据,这就是切片的数据共享问题。

    可以按照如下方式,避免切片的数据共享问题。

    1. slice3 := make([]int, 4)
    2. slice4 := slice3[1:3]
    3. slice3 = append(slice3, 0)
    4. slice3[1] = 2
    5. slice4[1] = 6
    6. fmt.Println("slice3:", slice3)
    7. fmt.Println("slice4:", slice4)

    虽然 slice2 是基于 slice1 创建的,但是修改 slice2 不会再同步到 slice1,因为 append 函数会重新分配新的内存,然后将结果赋值给 slice1,这样一来,slice2 会和老的 slice1 共享同一个底层数组内存,不再和新的 slice1 共享内存,也就不存在数据共享问题了。

    如下代码,虽然使用了append函数,但是没有重新分配内存空间,仍然存在数据共享问题。

    1. slice5 := make([]int, 4, 5)
    2. slice6 := slice5[1:3]
    3. slice5 = append(slice5, 0)
    4. slice5[1] = 2
    5. slice6[1] = 6

    slice5 容量为5,执行 append 没有进行扩容操作。

  • 相关阅读:
    deepin(深度)系统下qt5.12.0的程序打包发布给其他linux机器使用
    三、C++面向对象-类和对象那些你不知道的细节原理
    Typora美化思路分享
    2022软件测试3大发展趋势,看看你都知道吗?
    Error: EMFILE: too many open files : 往服务器一次性传输文件数太多
    课程表系列
    力扣刷题(代码回忆录)——动态规划
    科锐学习笔记-DEBUG命令使用解析及范例大全
    vue provide inject使用
    轻取软考45分之软考信息系统项目管理师范围管理​章节学习笔记
  • 原文地址:https://blog.csdn.net/suu_an/article/details/136323088