go迷之切片截取分析

2023-09-18 22:15:00

切片截取,有没有很迷?

目录

典型截取(两参数、三参数)及分析

迷之append参与截取及细节分析

关于截取时的初始索引是否从第一个位置开始的影响

修改原切片细节分析


典型截取(两参数、三参数)及分析

先看一个例子来表示一下切片截取(仅截取,无append):

    a := []int{1, 2}
    a0 := a[0:1]
    fmt.Printf("a0=%v, a0=%p, cap=%v\n", a0, a0, cap(a0)) // a0=[1], a0=0xc00009e070, cap=2
    fmt.Printf("a=%v, a=%p, cap=%v\n", a, a, cap(a))      // a=[1 2], a=0xc00009e070, cap=2
    
    b := a[0:1:1]
    fmt.Printf("a=%v, a=%p, cap=%v\n", a, a, cap(a))
    fmt.Printf("b=%v, b=%p, cap=%v\n", b, b, cap(b))    // b=[1], b=0xc00009e070, cap=1

    c := a[1:1]
    fmt.Printf("a=%v, a=%p, cap=%v\n", a, a, cap(a))
    fmt.Printf("c=%v, c=%p, cap=%v\n", c, c, cap(c))    // c=[], c=0xc00009e078, cap=1

    d := a[0:2:2]
    fmt.Printf("d=%v, d=%p, cap=%v\n", d, d, cap(d))

    e := a[1:2:2]
    fmt.Printf("e=%v, e=%p, cap=%v\n", e, e, cap(e))

    f := a[0:1]
    fmt.Printf("f=%v, f=%p, cap=%v\n", f, f, cap(f))
    fmt.Println(reflect.DeepEqual(f, b))     // true

    g := a[0:2]
    fmt.Printf("g=%v, g=%p, cap=%v\n", g, g, cap(g))    // g=[1 2], g=0xc00009e070, cap=2
    fmt.Println(reflect.DeepEqual(d, g))     // true 

    fmt.Println(reflect.DeepEqual(a, b))     // false
    fmt.Println(reflect.DeepEqual(a, d))     // true

结果:

a0=[1], a0=0xc00009e070, cap=2
a=[1 2], a=0xc00009e070, cap=2
a=[1 2], a=0xc00009e070, cap=2
b=[1], b=0xc00009e070, cap=1
a=[1 2], a=0xc00009e070, cap=2
c=[], c=0xc00009e078, cap=1
d=[1 2], d=0xc00009e070, cap=2
e=[2], e=0xc00009e078, cap=1
f=[1], f=0xc00009e070, cap=2
true
g=[1 2], g=0xc00009e070, cap=2
true
false
true

以上分析中包含了海量信息,能总结出方便理解的规律、能看透才是王道。 

从上面结果可以得到以下信息:
1,c和e的地址和其它的不同;
2,b虽然是一个新搞出来的切片,但b和a都是相同的地址a底层的数组b也在用,同时b是从a切片第一个位置开始(首地址)
3,c截取的也是a的元素组成的切片,但c是从第2个位置开始的,c和a的地址不同;
4,d截取的也是a的元素组成的切片,且d是从数组第1个位置开始的,d和a的地址相同;
5,e截取的也是a的元素组成的切片,是从数组第二个位置开始的,e和a的地址不同,e和c的地址相同
6,d的容量之所以为2,是因为截取时指定了截取的第三个参数为2,起始索引为0,则2-0=2,以此类推e的容量必然是1,因为2-1=1
7,d := a[0:2:2]这个操作包含了a的所有元素,是从首地址开始,同时长度和容量都和a相同,因此DeepEqual的结果为true
8,f和b容量不同,f和b的区别是截取的第三个参数一个指定了另一个是默认,b是1,f是2,那么容量不同为啥还能深度相等?
因为对于切片,类型相同、相同索引处元素相等,则深度相等
9,对于a0 := a[0:1]这样的仅截取操作,根本不会影响a切片,但截取操作在append中就可能有影响了。

这个代码例子虽然看起来简单没什么复杂操作,但却包罗万象不乏细节,下面是结论:

1,截取之后得到的切片是不是和原切片指向的地址相同,主要是看截取的首地址是否是从原切片的第一个元素开始,因为都是基于a来截取,底层数组一直在那放着,就看不同的人要截取多少(即东西是同一份东西,不同的人看到的视图不一样而已);
2,以a[d:e]为例,是指基于a切片,从其索引为d的位置开始、索引为e的位置(不包括)结束所获得的切片,所得切片的容量是a切片的原有容量
3,以a[d:e:f]为例,是指基于a切片,从其索引为d的位置开始、索引为e的位置(不包括)结束所获得的切片,所得切片的容量是f-d。

基于纯截取而没有其它操作的情形下,原切片不会被修改,基于以上案例和分析,我们可以得到切片截取时的地址、容量等变化。

迷之append参与截取及细节分析

下面是四个典型案例,需要仔细观察。

函数1:

func cut95() {
    fmt.Println("cut95 start")
    a := []int{1, 2}
    b := append(a[0:1], 3)
    //fmt.Println(a[1:2]) // [3]
    fmt.Printf("a= %v, %v, %p\n", a, cap(a), a) // a= [1 3], 2, 0xc00001a140
    fmt.Printf("b= %v, %v, %p\n", b, cap(b), b) // b= [1 3], 2, 0xc00001a140
    c := append(a[1:2], 4)
    fmt.Printf("a= %v, %v, %p\n", a, cap(a), a) // a= [1 3], 2, 0xc00001a140
    fmt.Printf("b= %v, %v, %p\n", b, cap(b), b) // b= [1 3], 2, 0xc00001a140
    fmt.Printf("c= %v, %v, %p\n", c, cap(c), c) // c= [3 4], 2, 0xc00001a170

    d := append(a[1:2], 4, 5, 6)                // 有人说扩容了就会怎么怎么样,来让它扩容一下
    fmt.Printf("a= %v, %v, %p\n", a, cap(a), a) // [1 3], 2, 0xc00001a140
    fmt.Printf("b= %v, %v, %p\n", b, cap(b), b) // [1 3], 2, 0xc00001a140
    fmt.Printf("c= %v, %v, %p\n", c, cap(c), c) // [3 4], 2, 0xc00001a170
    fmt.Printf("d= %v, %v, %p\n", d, cap(d), d) // [3 4 5 6], 4, 0xc0000121c0
    /*
        第1处截取操作:会导致a被修改
        第2处截取操作:a没有被修改
        第3处截取操作:a没有被修改
        注意除过第三次截取外其余截取之后的新切片均未发生扩容,和扩容无关;
        第三次截取扩容后地址发生了变化这是肯定了,因为容量需要扩充,原数组已不能满足要求、数组定长不能扩容,
        此时需要申请新数组(指针已变),再将数据拷贝到新切片;发生了扩容,但依旧原切片a没有变化。
    */
}

函数2:

func cut96() {
    fmt.Println("cut96 start")
    a := []int{1, 2}
    b := append(a[1:2], 3)
    //fmt.Println(a[1:2]) // [3]
    fmt.Println("a= ", a) // [1 2]
    fmt.Println("b= ", b) // [2 3]
    c := append(a[1:2], 4)
    fmt.Println("a= ", a) // [1 2]
    fmt.Println("b= ", b) // [2 3]
    fmt.Println("c= ", c) // [2 4]
    d := append(a[0:1], 5)
    fmt.Println("a= ", a) // [1 5]
    fmt.Println("b= ", b) // [2 3]
    fmt.Println("c= ", c) // [2 4]
    fmt.Println("d= ", d) // [1 5]
    /*
        第1处截取操作:a没有被修改
        第2处截取操作:a没有被修改
        第3处截取操作:会导致a被修改,并且a原本是1,2 其中2也没有了,截取的1和拼接的5组成了新的a
    */
}

 函数3:

  fmt.Println("cut97 start")
    a := []int{1, 2}
    b := append(a[0:1:1], 3)
    //fmt.Println(a[1:2]) // [2]
    fmt.Println("a= ", a) // [1 2]
    fmt.Println("b= ", b) // [1 3]
    c := append(a[1:2:2], 4)
    fmt.Println("a= ", a) // [1 2]
    fmt.Println("b= ", b) // [1 3]
    fmt.Println("c= ", c) // [2 4]
    /*
            第1处截取操作:a没有被修改
            第2处截取操作:a没有被修改
    */

函数4:

func cut98() {
    fmt.Println("cut98 start")
    a := []int{1, 2}
    b := append(a[0:1:2], 3) // 这里如果改为a[0:1:1] a不会被修改
    //fmt.Println(a[1:2]) // [2]
    fmt.Println("a= ", a) // [1 3]
    fmt.Println("b= ", b) // [1 3]
    c := append(a[1:2], 4)
    fmt.Println("a= ", a) // [1 3]
    fmt.Println("b= ", b) // [1 3]
    fmt.Println("c= ", c) // [3 4]
    d := append(a[0:2], 5)
    fmt.Println("a= ", a) // [1 3]
    fmt.Println("b= ", b) // [1 3]
    fmt.Println("c= ", c) // [3 4]
    fmt.Println("d= ", d) // [1 3 5]
    /*
            第1处截取操作:a被修改
            第2处截取操作:a没有被修改
            第3处截取操作:a没有被修改
    */
}

现象不细说了,一看代码便知,这里汇总一下结论:

给原切片做截取操作同时套append时,原切片会不会被跟着被修改(注意这里讨论的是还有append的情况下,并不是单纯截取),按下面步骤。

首先看截取后的切片是否和原切片相等(如长度为2容量也为2),如果相等则当然不变,如:
a := []int{1, 2}
d := append(a[0:2], 5)
上面换成  d := append(a[0:2:2], 5) 或  d := append(a[0:], 5) 或  d := append(a[:], 5) 效果都和a本身一样
这样的情况原切片a必然不变。

如果不满足1的情况,则此时看截取时所得的切片长度和容量

【下述的“所得切片”指的是截取后但未套append时的代称】

如果 所得切片的容量=原切片容量(注意没有套append时不牵扯扩容)
    a) 如果 数据长度<原切片数据长度(即仅截取了部分数据),则原切片会被修改; 
    如cut98()中的第一处截取append(a[0:1:2]、cut95()中的第一处截取append(a[0:1]
    但要注意的是,此时如果套了append并发生扩容时,即使满足此情况也不会修改原切片a,因为发生了扩容,原切片不能再满足,不会再做什么修改,cut95()中已说明。
    
    b) 如果 数据长度=原切片数据长度(即截取了所有数据,此时视图是原切片全部),则原切片不会被修改; 和上面第一个情况相同;
    
如果 所得切片的容量<原切片容量
    a) 数据长度<原切片数据长度(即仅截取了部分数据),则原切片不会被修改; 
    如cut97()中的第2处截取append(a[1:2:2],或cut96()中的第1、2处截取append(a[1:2]
    
    b) 数据长度=原切片数据长度(即截取了所有数据),这种情况不存在,编译都不能通过:Invalid index values, must be low <= high <= max

关于截取时的初始索引是否从第一个位置开始的影响

如cut98()中的第一处截取append(a[0:1:2]会修改原切片a;(和append(a[0:1]一样),但如果改为a[0:1:1] a则不会被修改
这两者都是从第一个位置开始截取,但结果显然不同,因此不能单以“截取时的初始索引是否从第一个位置开始”来定结果。

原切片会不会被修改,和截取时写成a[low:high]还是a[low:high:max]并无直接关系,和截取的初始索引是否从第一个位置开始也无直接关系。

修改原切片细节分析

那为什么所得切片的容量=原切片容量、且数据长度<原切片数据长度时,原切片就会被修改呢?

拿cut95的第一处来说:

    a := []int{1, 2}
    b := append(a[0:1], 3)

首先创建a切片,长度容量都是2,对应的底层数组是两个元素;

第二步分解,上面已经说明a[0:1]操作时不会修改a,a[0:1]相当于a[0:1:2],a[0:1]取得的值是1,对应的正是底层数组的第一个元素,a[0:1]这个切片本身地址和a一致,只是仅截取第一个元素

清楚了上面后接着做append操作,在第一个元素位置的基础上,拼接了元素3,相当于3这个元素覆盖掉了第二个位置上的2这个元素,因此底层数组变成了1、3

那你有没有这样想过:1后面拼接了3,为什么不变为1,3,2(即2被挤压了)   而是1,3呢? 为什么把2没有挤到后面去而是覆盖操作?

如果2被挤压、下标变化,意味着切片a的容量发生变化,但实际上拼接没有发生在a上,拼接是给b做了拼接,既然没给a拼接,a本身数据长度就不应该发生变化。

相反如果做了b := append(a[0:1], 3, 4, 5, 6)操作后,情形是a原本的容量已不足支撑新数据,系统经过计算申请了新数组,新数组适配b,然后才会塞数据,而且一样拼接不是发生在a上,a切片就不会再动了,不会再影响原切片。

更多推荐

孙哥Spring源码第24集

第24集处理AOP【视频来源于:B站up主孙帅sunsSpring源码视频】【微信号:suns45】1、谈一下你对ApplicationContext的理解BeanFactoryPostProcessorsBeanPostProcessor1.BDBeanFactoryPostProcessors、BeanPostPr

专利申请流程详解

专利申请的流程1、实用新型专利:是指对产品的形状、构造或者其结合所提出的适于实用的新的技术方案,指对有具体产品结构提出的改进或创造。与发明相比,实用新型专利申请对于技术的要求更低一点,在审查的时候不会进行详细的检索和对比,授权时间快,但实用新型的保护力度与发明专利的保护力度是不一样的。所需材料:请求书、说明书、权利要求

在内网部署docker工程总结

前言本次部署的内容主要包括:mysql,redis,nacos,java项目,前端项目,python项目。一安装docker环境首先在拥有网络环境的电脑上下载docker安装包,下载地址可以参考如下:https://download.docker.com/linux/static/stable/x86_64/下载完成之

RapidSSL的便宜单域名https证书

RapidSSL是Geotrusthttps证书品牌中的一款入门级https证书品牌,目前属于Digicert的子品牌。它是一款提供高性价比和广泛适用范围的https证书,无论是个人还是企业用户都可以轻松申请并快速验证。今天就随SSL盾小编了解RapidSSL旗下的单域名https证书。1.RapidSSL旗下的单域名

百望云亮相服贸会 重磅发布业财税融Copilot

小望小望,我要一杯拿铁!好的,已下单成功,请问要开具发票嘛?在获得确认的指令后,百小望AI智能助手按用户要求成功开具了一张电子发票!这是2023年服贸会国家会议中心·成果发布现场,百望云向与会嘉宾展示的业财税融Copilot产品的一个应用场景:在对接百望云后,咖啡师只需专注地制作咖啡,百小望AI智能助手即能完成接单、支

【深入浅出设计模式--命令模式】

深入浅出设计模式--命令模式一、背景二、问题三、解决方案四、试用场景总结五、后记一、背景命令模式是一种行为设计模式,它可以将用户的命令请求转化为一个包含有相关参数信息的对象,命令的发送者不需要知道接收者是如何处理这条命令,多个功能入口可以发送同一命令,避免多处多次实现相同功能的冗余代码。另外可以对命令进行延迟处理,或放

【RocketMQ】消息中间件学习笔记

【RocketMQ】消息中间件学习笔记【一】RocketMQ概述【1】MQ简介【2】MQ永用途(1)限流削峰(2)异步解耦(3)数据收集【3】RocketMQ介绍(1)RocketMQ特点(2)RocketMQ优势【4】RocketMQ基本概念(1)NameServer(2)Broker(3)生产者(Producer)

亚马逊六页纸

一、什么是亚马逊6页纸?亚马逊在内部管理实践中,特别是会议管理上,禁止使用PPT,而是使用一种简洁的「结构化备忘录」,也就是我们熟知的“六页纸”。二、具体解释1、Whatwedo?背景,什么情况下,我们做了什么。因为XXX产品即将量产,我们已启动XXX下一版升级产品的规划。今天开会的主题就是汇报与同步目前的规划情况。2

JS的WebAPI

WebAPI背景知识什么是WebAPI前面学习的JS分成三个大的部分ECMAScript:基础语法部分DOMAPI:操作页面结构BOMAPI:操作浏览器WebAPI就包含了DOM+BOM.什么是APIAPI是一个更广义的概念.而WebAPI是一个更具体的概念,特指DOM+BOM,所谓的API本质上就是一些现成的函数/对

【视觉SLAM入门】9.1 建图1---SLAM任务,稠密地图构建,立体视觉,RGBD,八叉树,点云地图等各种不同地图

"讷为君子,寡为吉人”1.立体稠密地图1.1地图构建1.2分析立体相机稠密建图效果2.RGB-D稠密地图2.1地图对比2.1.1八叉树地图3.建图?定位?孰轻孰重3.1鬼影问题3.2三维重建4.总结SLAM的功能:直到现在我们可以知道SLAM包含:定位,导航,避障,重建,交互。在不同的功能下也有不同的地图。之前的都是稠

计算机毕设 python图像检索系统设计与实现

文章目录0前言1课题简介2图像检索介绍(1)无监督图像检索(2)有监督图像检索3图像检索步骤4应用实例5最后0前言🔥这两年开始毕业设计和毕业答辩的要求和难度不断提升,传统的毕设题目缺少创新和亮点,往往达不到毕业答辩的要求,这两年不断有学弟学妹告诉学长自己做的项目系统达不到老师的要求。为了大家能够顺利以及最少的精力通过

热文推荐