• Go语言 切片 Slice


    切片是一种数据结构,更便于使用和管理数据集合。切片才是实际开发中最多使用的,它是围绕着动态数组的概念构建,可以按需自动增长和缩小。

    切片的内部实现和基础功能

    • 切片是一种数据结构
    • 切片可以按需动态增长和缩小
    • 切片的底层内存也是连续块分配,能享受及使用索引,迭代,垃圾回收

    内部实现

    切片是一个很小的对象,对底层数组进行了抽象,它是有3个字段的数据结构。

    • 指向底层数组的指针
    • 长度:切片访问的元素个数(长度)
    • 容量:切片允许增长的元素个数(长度)
      在这里插入图片描述

    创建切片和初始化

    Go语言在创建切片有好几种方法。
    是否知道切片需要的容量会决定要如何创建切片。

    make关键字创建切片

    内置的make函数,当使用make的时候 ,需要传入一个参数,指定切片的长度。

    // 创建一个字符串切片
    // 只指定了长度,长度和容量都是5
    slice := make([]string, 5)
    
    • 1
    • 2
    • 3

    只有指定长度,没有指定容量,那么切片的容量和长度相同。
    也可以分别指定长度和容量。

    // 指定了长度是3,容量是5
    slice := make([]string,3,5)
    // 设置访问索引3
    slice[3] = "apple"
    // 索引越界
    // panic: runtime error: index out of range [3] with length 3
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    这里分别指定了长度是3,容量是5的字符串切片。
    不允许创建容量小于长度的切片。
    底层的数组的长度是指定容量(这里底层数组是5的长度),但我们声明了切片长度是3,我们也只能访问3以内的索引,并不能访问3,4的索引。
    剩余的2个元素可以在后期操作中合并切片,可以通过切片访问这些元素。
    如果基于这个切片创建新的切片,新切片会和原有切片共享底层数组,也能通过后期操作来访问多作容量的元素。

    切片字面量创建切片

    通过切片字面量创建切片。

    // 创建字符串切片
    // 创建长度和容量都是2,根据初始化的值确定
    slice1 := []string{"apple", "huawei"}
    
    • 1
    • 2
    • 3

    也可以指定长度和容量创建

    // 创建长度和容量都是100的字符串切片
    slice2 := []string{1:"我是第2个元素",99: "我是第100个元素,其它元素都是空值"}
    fmt.Println(len(slice2))  //100
    
    • 1
    • 2
    • 3
    使用索引声明切片
    slice3 := []string{99: ""}
    fmt.Println(len(slice3)) // 100
    
    • 1
    • 2

    数组和切片的不同

    声明方式不同,数组必须指定长度,切片无需指定长度

    // 创建int类型长度为3的数组
    array1 := [3]int{10, 20, 30}
    // 创建int类型长度为3,容量为3的切片
    slice1 := []int{10, 20, 30}
    fmt.Println(array1) // [10 20 30]
    fmt.Println(slice1) // [10 20 30]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    数组和切片的底层数组结构上是致,数组的长度一经指定不能改变,但切片可以动态的改变数组底层长度。

    nil和空切片

    声明时不做任何初始化,就等同于创建了一个nil切片。

    nil切片
    描述一个不存在的切片

    // 创建nil整型切片
    var slice []int
    
    • 1
    • 2

    空切片
    空切片在底层数组包含0个元素,没有分配任何空间。空集合

    // make创建长度为0的空切片
    var slice2 = make([]int, 0)
    
    // 字面量方式 创建
    slice3 := []int{}
    
    • 1
    • 2
    • 3
    • 4
    • 5

    无论是nil切片还是空切片,都可以使用内置函数,append,len和cap。

    使用切片

    赋值和切片

    访问和赋值
    切片的赋值和访问与数组完全一样。

    // 声明一个int类型的切片,初始长度为8,容量为5
    slice := []int{10, 20, 30, 40, 50}
    fmt.Println(slice[1])  // 20
    // 改变索引1的值为200
    slice[1] = 200
    fmt.Println(slice[1]) // 200
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    使用切片创建切片
    创建切片的语法:[ start, end, cap ]

    • start 开始索引
    • end 结束索引
    • cap 指定容量
    // 创建一个整型切片,长度和容量都是5
    slice := []int{10, 20, 30, 40, 50}
    // 从slice切片中创建新切片
    // 长度是2,
    newSlice := slice[1:3]
    
    • 1
    • 2
    • 3
    • 4
    • 5

    在这里插入图片描述
    slice切片能看到底层数组的5个元素,而newSlice切片则不行。
    newSlice底层的数组容量只有4个元素。

    切片创建的切片会和源切片共享同一个底层数组。如果一个切片修改了底层数组的共享部分,另一个切片也同样生效。

    更多示例:
    在这里插入图片描述

    如何计算长度和容量

    假设底层数组容量是K,那么计算slice[ i:j ]方法如下:

    • 长度: j - i, 这里的j是指新切片中的end值
    • 容量: k - i,这里的k是指源切片中的容量值
      示例
    // 创建一个整型切片,长度和容量都是5
    slice := []int{10, 20, 30, 40, 50}
    // 创建一个新切片
    newSlice := slice[ 1: 3] 
    // 计算方法 k = slice切片的容量 5
    // newSlice的长度: 3 - 1 = 2
    // newSlice的容量: 5 - 1 = 4
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    切片增长

    append
    先介绍append的使用,接下来再描述增长动态扩容的细节
    要使用append,需要一个被操作的切片和一个要增加的值。
    示例:

    // 声明一个int类型的切片,长度为5,容量为5
    slice := []int{10, 20, 30, 40, 50}
    newSlice := append(slice, 60)
    // 这里newSlice的长度是6容量是10 
    
    • 1
    • 2
    • 3
    • 4

    debug调试能看到内存中的长度和容量
    在这里插入图片描述
    append操作成功后一定会增加新切片的长度,但容量是动态的有可能会有变化也有可能不会有变化,取决于被操作的切片的可用容量。
    如上示例,slice的切片长度是5,容量也是5,这里新加一个元素,长度变成6了,那么容量必须也动态扩容,当前Go语言中容量扩容会在上一个容量的基础上乘以2,这里newSlice的切片容量即5*2=10。同时会创建一个新的底层数组,不同于源slice底层数组。
    这里如果我们继续往newSlice中添加数据,那么只是长度不超过10,那么切片容量也不会有变化,底层数组也不会有变化。

    在这里插入图片描述
    底层数组容量动态扩展的原则是:
    切片容量小于1000个元素的时候 ,会成倍的增长,元素超过1000的时候 ,会1.25倍的增长。

    append中的…运算符
    内置函数append也是一个可变参数的函数,可以在一次调用传递多个追加的值。
    如果使用…运算符,可以将一个切片的所有元素追加到另一个切片里。
    示例:

    slice := []string{"apple", "huawei", "oppo", "xiaomi"}
    slice2 := []string{"black", "blue", "green"}
    fmt.Println(append(slice2, slice...))
    // 输出结果:[black blue green apple huawei oppo xiaomi]
    
    • 1
    • 2
    • 3
    • 4

    先来看一个案例:
    这里的第2个切片使用append,会不小心把第1个切片的值也改变了。
    原因是:因为共享了同一个底层数组,第2个切片还有容量可以使用,于就在赋值的时候直接把值赋在了这个位置。

    在这里插入图片描述
    如何解决这个问题,可以引入第3个参数

    创建切片时的3个索引(cap)

    用来控制新切片的容量。目的不是为了增加容量是为了限制容量。
    将长度和容量设置一样,就可以让新切片的第1个append操作创建一个新的底层组,这样就不会和源切片共享一个底层数组,改变值时也不会影响到源切片。

    	slice := []string{"apple", "huawei", "oppo", "xiaomi"}
    	// 对第三元素做切片,并限制容量
    	newslice := slice[2:3:3]
    	newslice = append(newslice, "add1")
    	fmt.Println(slice) // [apple huawei oppo xiaomi]
    	fmt.Println(newslice) // [oppo add1]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    在这里插入图片描述

    迭代切片

    range关键字

    range可以配置for循环来迭代切片的元素。
    示例:

    slice := []string{"apple", "huawei", "oppo", "xiaomi"}
    for k, v := range slice {
    	fmt.Printf("index: %d, value: %s\n", k, v)
    }
    // 输出结果:
    /*
    index: 0, value: apple
    index: 1, value: huawei
    index: 2, value: oppo  
    index: 3, value: xiaomi
    */
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    迭代会返回2个值 ,第1个值是当前迭代到的索引位置,第2个是该位置对应元素值 的一份副本。
    注意:
    range创建了每个元素的副本,而不是直接返回对该元素的引用。
    通过一个例子来看一下

    slice := []string{"a1", "a2", "a3", "a4"}
    	for index, v := range slice {
    		fmt.Printf("index: %d, value: %s Value-Addr: %X , ElementAddr: %X\n", index, v, &v, &slice[index])
    	}
    // 打印结果
    /**
    index: 0, value: a1 Value-Addr: C000040250 , ElementAddr: C00004A040
    index: 1, value: a2 Value-Addr: C000040250 , ElementAddr: C00004A050
    index: 2, value: a3 Value-Addr: C000040250 , ElementAddr: C00004A060
    index: 3, value: a4 Value-Addr: C000040250 , ElementAddr: C00004A070
    */
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    可以看到Value-Addr的地址都是同一个,因为迭代返回的变量是一个迭代过程中产生的赋值的新变量。
    要想获取每个元素的地址,可以使用切片变量和索引值。

    多维切片

    和数组一样,切片是一维的。类似数组一样可以组合成多维的。
    略…

    在函数中传递切片

    细节描述:

    • 在函数中传递切片就是要在函数间以值的方式传递切片。
    • 切片的尺寸很小,在函数间复制和传递切片成本也很低。在64位的架构机器上,一个切片需要24个字节的内存,长度和容量字段分别需要8字节。
    • 切片关联的数据包含在底层数组里面,不属于切片本身,所以将切片复制到任意函数的时候,对底层数组大小都不会有影响。
    • 复制时只复制切片本身,不会涉及底层数组。
  • 相关阅读:
    Java之正则表达式的详细解析
    Python之字符串格式化
    字正腔圆,万国同音,coqui-ai TTS跨语种语音克隆,钢铁侠讲16国语言(Python3.10)
    JavaSE:String类
    电子科技大学 数学专业-功不唐捐,玉汝于成
    多尺度深度特征(下):多尺度特征学习才是目标检测精髓(论文免费下载)...
    UDS诊断入门
    K8S | 容器和Pod组件
    【Linux常用命令10】用户管理和文件权限命令
    基于x86 平台移植opencv4.7.0
  • 原文地址:https://blog.csdn.net/ziyi813/article/details/127931696