Go的切片

Go的切片是一个高效而强大的处理数据序列的类型,相比来说我们使用数组的时候更少,因为数组一旦定义它的长度不能再改变,使用起来不够灵活。但任何事情都会有副作用,就像之前说过的Go的语法糖带来便利的同时也隐藏了一些坑。有时候是出乎意料的特性,有时候是性能的损失,稍不注意可能就会陷入其中,给你一种这语言有“严重”的缺陷的感觉,严重了甚至很可能让你放弃使用它,之前就看到早期国外有开发团队就出现过这种情况。

基本认识
slice可以从数组中生成,也可以直接创建,在创建的时候可以指定slice的长度和容量,如果没有指定容量(make的第三个参数),那么容量就默认和长度相同。也可以不通过make创建,而是直接通过var的方式声明一个零值的slice,则它的长度和容量都为0。

var s1 []byte = buffer[10:20]
var s2 []int
var s3 = make([]int, 10)
var s4 = make([]int, 10, 20)

slice底层元素是由一个数组保存着,它的容量就是这个数组的长度,你可以可以通过cap获得。如果在使用slice的过程,元素的数量超过了容量,那么就需要创建新的数组。这是一个值得注意的地方,如果你初始的时候就可以确定元素的最大数量,那么最好设置slice的容量的值,这样避免数组的重新分配和数据拷贝,提高程序的性能,但这里如果使用不当你可能会陷入困境,下面我们详细说。

基本使用

索引

slice和数组一样,同样可以通过索引访问某个位置的元素。但如果超出slice的索引最大值,就会导致panic。

panic: runtime error: index out of range
想要删除的时候,就可以使用索引

s2 := append(s1[:2], s1[3:]...)

append

通过append方法可以往slice增加元素,需要清楚的是append后的返回值是增加元素后的slice,和原始的slice是不同的,尽管它们底层的数组可能相同(如果容量足够,元素就继续增加底层数组中,如果容量不够,则结果slice就会创建新的数组)。

这里还有一个地方,就是你同样可以同时append多个元素:

slice1 = append(slice1, x1, x2)
slice1 = append(slice1, slice2...)

copy

扫描二维码关注公众号,回复: 2377851 查看本文章

copy的返回结果为 dst和src的长度的较小值。这也就是说,如果dst的长度大,则将src全部元素都复制到dst中。如果src的长度大,则将src的len(dst)个元素复制到dst中。

copy(s1,s2)
深入坑

上面提到slice底层使用数组,而在使用过程这个数组可能在多个slice中共用,这就带来潜在的问题。

更改连锁

test1 := make([]int, 0)
for i := 0; i < 8; i++ {
     test1 = append(test1, i)
}
test2 := test1
test2[0] = 99
fmt.Println(test1, test2)//[99 1 2 3 4 5 6 7] [99 1 2 3 4 5 6 7]

当我们修改test2时,test1也会被修改。想要原来的不被修改,那么你就需要新建一个test2,这里技巧就是可以使用copy。

隐藏数据

test1 := make([]int, 0)
for i := 0; i < 8; i++ {
    test1 = append(test1, i)
}
test2 := test1[0:2]
test3 := test1[0:3]
test2 = append(test2, 99)
fmt.Println(test1, test2, test3)//[0 1 99 3 4 5 6 7] [0 1 99] [0 1 99]

这个例子test2和test3都使用相同的数组test1,而且它们的容量都是8,不同的是它们的长度分别是2和3。

当往test2增加增加一个元素的时候,它事实上将元素放在的数组的索引为2的位置。所以这会对原始数组和test3都有影响。

copy的覆盖

调用copy方法的时候也可能会产生覆盖原数组

test1 := make([]int, 0)
for i := 0; i < 8; i++ {
    test1 = append(test1, i)
}
test2 := test1[0:2]
test3 := test1[2:6]
copy(test2, test3)
fmt.Println(test1, test2, test3)// [2 3 2 3 4 5 6 7] [2 3] [2 3 4 5]

make使用的规则

Ok,说了这么多,我都懂了,使用make来分配,知道了长度那么就事先指定长度。然而一不小心还是会出现错误。(PS:最近在开发X系统时就不小心碰到了,导致程序崩溃!)

// 我要一个长度为8的slice
test1 := make([]int, 8)
for i := 0; i < 8; i++ {
    test1 = append(test1, i)
}

fmt.Println(test1)// [0 0 0 0 0 0 0 0 0 1 2 3 4 5 6 7]

发生了什么?为什么前面这么多0?

这是因为当你预先给定了长度,那么这个长度之内的所有值都会被写入默认的类型的值,相当于append了指定长度个数的默认值。所以,如果你要指定长度,在后面的使用过程中,就要通过索引赋值,而不是append了。这是使用make的一个规则。切记!!!

一个切片是一个数组片段的描述。它包含了指向数组的指针、片段的长度和容量(片段的最大长度)

Array和Slice的区别:

Array是值类型,固定长度,大小是类型的一部分,当一个数组变量被赋值或者被传递的时候,实际上会复制整个数组。而Slice在新元素加入的时候可以增加长度(增长到容量的上限)

slice 一个切片是一个数组片段的描述。它包含了指向数组的指针,片段的长度和容量(底层数组的长度)。 slice内部结构
这里写图片描述

切片
切片本身是个只读对象,其工作机制类似数组指针的一种包装。可以基于数组或数组指针创建切片,以开始和结束索引位置确定引用的数组片段。不支持反向索引,世纪范围是一个右半开区间。属性 cap 表示切片所引用数组片段的真是长度,len 用于限定可读的写元素数量。另外,数组必须 addressable,否则会引发错误。
与数组区别:

1、切片创建不需要提前声明长度;而数组需要声明,操作数组索引大于该数组长度,会产生越界的异常。
2、切片可以通过 make([]int, 3, 5) 方式创建,第一个参数为类型,第二个参数为长度(len),第三个参数为容量(cap)
3、切片的长度(len)和 容量(cap)两个属性不一定相等;而数组的两个属性一定相等。切片底层是数组的一部分或者全部, cap 值为从切片引用该底层数组开头部分到该底层数组结尾的长度,而 len 值为该切片引用的底层数组部分的长度。
4、切片是引用类型,而数组是值类型。说白了就是切片传递的是引用,只要对任意一个地方修改原切片值也会改变;而数组是值类型,每次传递相当于重新分配内存创建了一个新的数组,各个之间相互独立不影响。
5、切片不支持比较操作,即使元素类型支持也不行,仅可以判断您是否是 nil

优势

很显然,切片只是很小的结构体对象,用来代替数组传参可以避免复制开销
make 函数允许在运行期动态制定数组长度,绕开了数组类型必须使用编译期常量的限制。
然而,并非所有时候都适合使用切片代替数组,因为切片底层数组可能会在堆上分配内存, 而且小数组在栈上拷贝的消耗也未必就比 make 代价大。

append

向切片尾部添加数据(slice[len]),返回新的切片对象(如果超过 cap 限制,则为心切片独享重新分配内存和数组,所以要返回新的切片对象)。向 nil 切片追加数据时,会为其分配底层数组内存。
注意:

是超出切片 cap 限制,而非超出底层数组长度限制,因为 cap 可小于数组长度。
新分配数组长度是原 cap 的 2 倍,而非原数组的 2 倍(并非总是 2 倍,对于较大的切片,会尝试扩容 1/4,以节约内存)
因为存在重新分配底层数组的缘故,建议预留足够空间,避免多次分配复制的内存开销

copy

在两个切片对象间复制数据,允许指向同一底层数组,允许目标区间重叠。最终所复制长度以较短的切片长度(len)为准。

猜你喜欢

转载自blog.csdn.net/coolwriter/article/details/81197191