• slice切片底层原理


    一、简介

    切片本身并不是动态数组或者数组指针。它内部实现的数据结构通过指针引用底层数组,设定相关属性将数据读写操作限定在指定的区域内。切片本身是一个只读对象,其工作机制类似数组指针的一种封装。

    切片(slice)是对数组一个连续片段的引用,所以切片是一个引用类型。这个片段可以是整个数组,或者是由起始和终止索引标识的一些项的子集。需要注意的是,终止索引标识的项不包括在切片内。

    和数组不同的是,切片的长度可以在运行时修改,最小为 0 最大为相关数组的长度:切片是一个长度可变的数组。

    二、数据结构

    结构体:

    type slice struct {
        array unsafe.Pointer
        len   int
        cap   int
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    如图:
    在这里插入图片描述
    在这里插入图片描述
    分析: 切片的结构体由3部分构成,Pointer 是指向一个数组的指针,len 代表当前切片的长度,cap 是当前切片的容量。cap 总是大于等于 len 的。

    三、创建切片

    make 创建:

    slice := make([]int, 4, 6)
    
    • 1

    如图:
    在这里插入图片描述

    分析: 上图是用 make 函数创建的一个 len = 4, cap = 6 的切片。内存空间申请了6个 int 类型的内存大小。由于 len = 4,所以后面2个暂时访问不到,但是容量还是在的。这时候数组里面每个变量都是0

    字面量创建切片例:

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

    如图:
    在这里插入图片描述
    分析: 这里是用字面量创建的一个 len = 6,cap = 6 的切片,这时候数组里面每个元素的值都初始化完成了。需要注意的是 [ ] 里面不要写数组的容量,因为如果写了个数以后就是数组了,而不是切片了。

    通过数组创建切片如图:
    在这里插入图片描述
    分析: 上图就 Slice A 创建出了一个 len = 3,cap = 3 的切片。从原数组的第二位元素(0是第一位)开始切,一直切到第四位为止(不包括第五位)。同理,Slice B 创建出了一个 len = 2,cap = 4 的切片。

    四、nil 和空切片

    语法:

    var slice []int
    
    • 1

    如图:
    在这里插入图片描述
    分析: nil 切片被用在很多标准库和内置函数中,描述一个不存在的切片的时候,就需要用到 nil 切片。比如函数在发生异常的时候,返回的切片就是 nil 切片。nil 切片的指针指向 nil。

    语法:

        silce := make( []int , 0 )
        slice := []int{ }
    
    • 1
    • 2

    如图:
    在这里插入图片描述
    分析: 空切片和 nil 切片的区别在于,空切片指向的地址不是nil,指向的是一个内存地址,但是它没有分配任何内存空间,即底层元素包含0个元素。

    注意: 不管是使用 nil 切片还是空切片,对其调用内置函数 append,len 和 cap 的效果都是一样的。

    五、扩容策略

    如果切片的容量小于 1024 个元素,于是扩容的时候就翻倍增加容量。一旦元素个数超过 1024 个元素,那么增长因子就变成 1.25 ,即每次增加原来容量的四分之一。

    注意: 扩容扩大的容量都是针对原来的容量而言的,而不是针对原来数组的长度而言的。

    如图:
    在这里插入图片描述
    在这里插入图片描述

    六、新数组还是老数组

    如图:
    在这里插入图片描述

    分析: 情况一是由于原数组还有容量可以扩容,所以执行 append() 操作以后,会在原数组上直接操作,所以这种情况下,扩容以后的数组还是指向原来的数组。情况二是是因为原来数组的容量已经达到了最大值,再想扩容, Go 默认会先开一片内存区域,把原来的值拷贝过来,然后再执行 append() 操作。这种情况丝毫不影响原数组。

    注意: 建议尽量避免情况一,尽量使用情况二,避免 bug 产生。

    七、切片遍历

    例:

    package main
    
    import (
    	"fmt"
    )
    
    func main() {
    	slice := []int{10, 20, 30}
    	for index, value := range slice {
    		fmt.Printf("value = %d , value-addr = %x , slice-addr = %x\n", value, &value, &slice[index])
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    结果:

    value = 10 , value-addr = c00000a0b0 , slice-addr = c00000c3c0
    value = 20 , value-addr = c00000a0b0 , slice-addr = c00000c3c8
    value = 30 , value-addr = c00000a0b0 , slice-addr = c00000c3d0
    
    • 1
    • 2
    • 3

    如图:
    在这里插入图片描述
    分析:由于 Value 是值拷贝的,并非引用传递,所以直接改 Value 是达不到更改原切片值的目的的,需要通过 &slice[index] 获取真实的地址。

  • 相关阅读:
    python安装与配置
    第15集丨知行合一
    ORB-LSAM2:ComputeKeyPointsOctTree()提取特征:maxY = iniY + hCell + 6 为怎么是+6而不是+3?
    2022年新能源汽车行业分析
    Docker镜像文件介绍启动tomcat
    Redis部署方式(三)主从模式
    容器云平台规划部署架构设计
    基于ssm的大学生心理健康系统设计与实现
    Jmeter和Postman那个工具更适合做接口测试?
    [答疑]《实现领域驱动设计》的译者其实没错?(二)
  • 原文地址:https://blog.csdn.net/change_any_time/article/details/125454501