slice
切片是不定长的特定元素类型的序列,可以理解为动态数组
Go语言中,数组在传递的时候,传递的是原数组的拷贝,对大数组来说,内存代价会非常大,影响性能。
传递数组指针可以解决这个问题,但是数组指针也有一个弊端:原数组的指针指向改变了,那函数里面的指针指向也会跟着改变,某些情况下,可能会产生意想不到的bug。slice的出现,便是为了解决这个问题。
一、特点
- 长度不固定
- 切片是引用类型,一般来说是浅拷贝
- 切片本身不能存储任何数据,都是底层数组存储数据,修改切片的时候修改的是底层数组中的数据,切片一旦扩容,会指向一个新的底层数组,内存地址也就随之改变。
- 底层实现是一个结构体,包括长度、容量和一个指向实际数组的unsafe.Pointer指针
|
|
注意:
底层数组是可以被多个 slice 同时指向的,因此对一个 slice 的元素进行操作是有可能影响到其他 slice 的。
二、扩容规则
- 如果扩容需求大于当前容量的两倍,扩容后的容量为所需的最小容量
- 当前切片长度<1024,扩容当前容量为2倍,
- 当前切片长度>1024,每次扩容当前容量的1.25倍,循环扩容直至容量满足需求
切片扩容之后,指向匿名数组的指针地址会发生变化。
1.18 引入了新的扩容规则,首先 1024 的边界不复存在,取而代之的常量是 256 。超出256的情况,也不是直接扩容25%,而是设计了一个平滑过渡的计算方法,随着容量增大,扩容比例逐渐从100%平滑降低到25%,从 2 倍平滑过渡到 1.25 倍。
为什么要这样设计?
避免追加过程中频繁扩容,减少内存分配和数据复制开销,有助于性能提升。
计算出了新容量之后,还没有完,出于内存的高效利用考虑,还要进行内存对齐。进行内存对齐之后,新 slice 的容量是要 大于等于 老 slice 容量的 2倍或者1.25倍。
三、声明与赋值
|
|
注意:
- 只声明的切片 等于nil,长度与容量都为0
- make([]string, 0, 0)赋值的切片,长度与容量都为0,但不是nil
- make会用零值0初始化所有元素, cap可心省略(默认等于长度)
四、切片slice和数组array的关系
-
切片slice的底层是对数组array的引用;
-
切片可以引用数组的部分元素或者全部元素;
-
切片slice的指针指向的是切片的第一个元素的内存地址,也就是该元素对应的数组的元素的内存地址。
五、切片操作
- len,cap
可以查看切片的长度与容量
- append
s = append(s, “x”)
s作为参数传给函数append是值传递,是结构体的拷贝,底层数组的指针一样
底层数组加了一个元素,s拷贝的长度也会跟着变,但s的长度没变啊,所以s要重新赋值
另外如果扩容了s拷贝的底层数组指针会变, 长度,容量也会变,但原来的s什么都不会变,所以s也要重新赋值
值的过程复制一个新的切片,这个切片也指向原始变量的底层数组。
函数中无论是直接修改切片,还是append创建新的切片,都是基于共享切片底层数组的情况作为基础,
最外面的原始切片是否改变,取决于函数内的操作和切片本身容量,是否修改了底层数组。
- 如果要修改切片的值,那么一定对底层数组做了修改,为影响到函数外的切片
- 如果是append操作,则要看切片是否扩容
- 切片没有进行扩容,那么会直接添加或修改切片指向底层数组中后一位的值,故底层数组会受到改变,函数外切片改变;
- 而如果进行扩容,则会导致切片指向一个新的底层数组,一切修改都对函数外的原切片无影响
- 访问
切片的引用方式是[ start, end )半闭区间的模式,即索引start可以引用到,而end是不能引用到的
start可以没有,默认为0;end可以没有默认cap
-
删除
1 2 3 4 5 6 7 8 9 10
s := []int{0, 1, 2, 3, 4, 5} // 删除go切片首尾元素的方法 s = s[1:] //利用切片引用并重新赋值的方法,删除掉首尾元素,如果想删除两个,可以用s=s[2:] fmt.Println(s) //[1,2,3,4,5] s = s[0:(len(s) - 1)] //删除末尾的元素 fmt.Println(s) //[1,2,3,4] // 接下来,我们利用append()方法来删除切片中间位置的元素 s = append(s[:1], s[2:]...) fmt.Println(s) //[1,3,4]
-
复制
切片共用底层数组,修改的话有可能影响原来的切片,如果我们不想这样,可以使用copy
1 2 3 4 5 6 7 8 9
s0 := []int{1, 0, 3, 2, 6, 5} s1 := make([]int, len(s0)) s2 := s0 copy(s1, s0) fmt.Printf("s0的内存地址为%p,s1的内存地址为%p,s2的内存地址为%p \n", &s0, &s1, &s2) s1[0] = 100 fmt.Printf("s1元素修改后,s1的值为%v, s0的值为%v \n", s1, s0) s0[0] = 99 fmt.Printf("s0元素修改后,s0的值为%v,s2的值为%v,s1的值为%v \n", s0, s2, s1)
s0,s2相互影响,而s1则跟s0,s2没有关系