Go语言学习8:深入理解切片slice

slice定义

slice是个结构体,源码如下:

// runtime/slice.go
type slice struct {
    array unsafe.Pointer // 元素指针
    len   int // 长度 
    cap   int // 容量
}

slice 共有三个属性:

  • 指针,指向底层数组;
  • 长度,表示切片可用元素的个数,也就是说使用下标对 slice 的元素进行访问时,下标不能超过 slice 的长度;
  • 容量,底层数组的元素个数,容量 >= 长度。在底层数组不进行扩容的情况下,容量也是 slice 可以扩张的最大限度。

在这里插入图片描述

注意,底层数组是可以被多个 slice 同时指向的,因此对一个 slice 的元素进行操作是有可能影响到其他 slice 的。

创建方法

创建 slice 的方式有以下几种:

序号方式代码示例
1直接声明varvar slice []int
2newslice := *new([]int)
3字面量:=slice := []int{1,2,3,4,5}
4makeslice := make([]int, 5, 10)
5从切片或数组“截取”slice := array[1:5]slice := sourceSlice[1:5]

不同方式创建出来的切片分为 nil切片 和 空切片。

  • nil切片的长度和容量都为0,和nil比较的结果为true。
  • 空切片的长度和容量也都为0,但是所有的空切片的数据指针都指向同一个地址 0xc42003bda0。空切片和 nil 比较的结果为false。
创建方式nil切片空切片
方式一var s1 []intvar s2 = []int{}
方式二var s4 = *new([]int)var s3 = make([]int, 0)
长度00
容量00
和 nil 比较truefalse

截取

截取也是比较常见的一种创建 slice 的方法,可以从数组或者 slice 直接截取,当然需要指定起止索引位置。新 slice 和老 slice 或数组共用底层数组,新老 slice 对底层数组的更改都会影响到彼此。

值得注意的是,新老 slice 或者新 slice 老数组互相影响的前提是两者共用底层数组,如果因为执行 append 操作使得新 slice 底层数组扩容,移动到了新的位置,两者就不会相互影响了。所以,问题的关键在于两者是否会共用底层数组。

slice := data[2:4] // data[low, high]
slice := data[2:4:6] // data[low, high, max]

一般来说,截取后的第一个元素是low索引的元素,最后一个元素是high-1索引的元素。

最大容量则只能是索引 max-1 处的元素。high 和 max 必须在老数组或者老 slice 的容量(cap)范围内。

当 high == low 时,新 slice 为空slice

截取的例子

package main

import "fmt"

func main() {
    slice := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
    s1 := slice[2:5]
    s2 := s1[2:6:7]

    s2 = append(s2, 100)
    s2 = append(s2, 200)

    s1[2] = 20

    fmt.Println(s1)
    fmt.Println(s2)
    fmt.Println(slice)
}

先看下代码运行的结果:

[2 3 20]
[4 5 6 7 100 200]
[0 1 2 3 20 5 6 7 100 9]

我们来走一遍代码,初始状态如下:

slice := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
s1 := slice[2:5]
s2 := s1[2:6:7]

s1 从 slice 索引2到索引4,长度为3,容量默认到数组结尾,为8。 s2 从 s1 的索引2到索引5,容量到索引6,为5。
在这里插入图片描述
接着,向 s2 尾部追加一个元素 100:

s2 = append(s2, 100)

s2 容量刚好够,直接追加。不过,这会修改原始数组对应位置的元素。这一改动,数组和 s1 都可以看得到。
在这里插入图片描述
再次向 s2 追加元素200:

s2 = append(s2, 100)

这时,s2 的容量不够用,该扩容了。于是,s2 另起炉灶,将原来的元素复制新的位置,扩大自己的容量。并且为了应对未来可能的 append 带来的再一次扩容,s2 会在此次扩容的时候多留一些 buffer,将新的容量将扩大为原始容量的2倍,也就是10了。
在这里插入图片描述
最后,修改 s1 索引为2位置的元素:

s1[2] = 20

这次只会影响原始数组相应位置的元素。它影响不到 s2 了,人家已经远走高飞了。
在这里插入图片描述
再提一点,打印 s1 的时候,只会打印出 s1 长度以内的元素。所以,只会打印出3个元素,虽然它的底层数组不止3个元素。

append

append 函数的参数长度可变,因此可以追加多个值到 slice 中,还可以用 ... 传入 slice,直接追加一个切片。append函数返回值是一个新的slice。

slice = append(slice, elem1, elem2)
slice = append(slice, anotherSlice...)

使用 append 可以向 slice 追加元素,实际上是往底层数组添加元素。但是底层数组的长度是固定的,如果索引 len-1 所指向的元素已经是底层数组的最后一个元素,就没法再添加了。

这时,slice 会迁移到新的内存位置,新底层数组的长度也会增加,这样就可以放置新增的元素。同时,为了应对未来可能再次发生的 append 操作,新的底层数组的长度,也就是新 slice 的容量是留了一定的 buffer 的。否则,每次添加元素的时候,都会发生迁移,成本太高。

其实 nil slice 或者 empty slice 都是可以通过调用 append 函数来获得底层数组的扩容。


转自:深度解密Go语言之Slice

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值