golang之slice

什么是slice?

slice翻译过来是切片,它和数组(array)非常类似,可以通过下表来访问,但是切片要比数组灵活很多,因为切片可以自动扩容。

// runtime/slice.go
type slice struct {
    array unsafe.Pointer // 指向底层数组的指针
    len   int // 长度 
    cap   int // 容量
}

我们看到slice底层是一个结构体,有三个字段,分别是指向底层数组的指针、长度、容量。而显然这三者都占8个字节(64位机器上),那么这就意味着无论什么样的切片,大小都是24个字节,不信我们来看一下。

package main

import (
    "fmt"
    "unsafe"
)

func main() {
    s1 := make([]int, 3, 10)
    s2 := make([]int, 0, 0)
    s3 := make([]int, 1, 100)
    s4 := make([]int, 3, 2000)
    s5 := make([]int, 30, 30)
    s6 := []int{}
    fmt.Println(
        unsafe.Sizeof(s1),
        unsafe.Sizeof(s2),
        unsafe.Sizeof(s3),
        unsafe.Sizeof(s4),
        unsafe.Sizeof(s5),
        unsafe.Sizeof(s6),
        ) //24 24 24 24 24 24
}

所以slice底层还是使用数组存储的,slice只是一个结构体,保存了底层数组的首地址、元素的个数、以及容量。我们可以像数组一样通过索引来对slice进行访问,并且我们看到了容量,一旦当大小超过了容量的时候就会进行扩容。

以s := make([]int, 3, 5)为例

我们访问切片的元素等价于访问底层数组的元素,修改切片里面元素的值等价于修改底层数组里面元素的值。但是注意:底层数组是可以被多个slice同时指向的,因此修改一个其中一个slice,也会影响其他的slice,因为这些切片指向的都是同一个底层数组。

创建slice

创建slice有很多种方式:

直接声明:var s []int

这种直接声明的方式,创建出来的实际上一个nil slice,我们说像slice、map、channel,直接声明的话,是不会分配内存的,返回的是一个空指针。

**使用new:var s = *new([]int)**

package main

import (
    "fmt"
)

func main() {
    var s = *new([]int)
    fmt.Println(s) // []

    var s1 []int
    fmt.Println(s1) // []

    s = append(s, 1)
    s1 = append(s1, 1)
    fmt.Println(s, s1) // [1] [1]
}

我们可以使用new关键字,我们这个和直接声明得到的结果是一样的,但是注意:这只是打印显示的一样,但是:s对应的底层数组已经被创建了,而s1对应底层数组没有被创建。可我们发现对s1使用append居然也可以,这是因为使用append的话,如果没有分配底层数组的话,那么会自动先帮你分配一个大小、容量都为0的底层数组,然后再把元素append进去。

字面量:var s = []int{1, 2, 3}

可以直接向创建普通元素一样,直接创建slice。

package main

import (
    "fmt"
)

func main() {
    var s = []int{1, 2, 3}
    fmt.Println(s)

    //另外还可以指定索引
    //5:6表示索引为5地方赋值为6,那么这就意味着它前面的元素索引最多为4
    //如果达不到4的话,那么没指定的则为0,后面的元素依次往后填充
    var s1 = []int{1, 2, 5:6, 3, 4, 5, 6}
    fmt.Println(s1) // [1 2 0 0 0 6 3 4 5 6]
    
    //但是注意了:可不能这样子
    //var s2 = []int{1, 2, 3, 2:1}
    //这样是会报错的,因为索引为2的地方已经有3了,然后后面又指定了2:1,就会报出索引重复的错误
}

使用make:var s = make([]int, len, cap)

最常用,make函数专门使用来创建slice、map、channel的,传入类型、大小、容量。如果不指定容量,那么默认容量和大小一样。

package main

import (
    "fmt"
)

func main() {
    var s = make([]int, 3, 5)
    //创建长度为3,容量为5的切片
    //既然长度为3,说明已经有3个元素了,这3个元素就是默认的0值
    fmt.Println(s)  //[0 0 0]
}

从数组中拷贝

package main

import (
    "fmt"
)

func main() {
    //创建元素个数为6的数组
    var arr = [...]int{5:1}
    fmt.Println(arr) // [0 0 0 0 0 1]
    //通过切片从数组中拷贝
    //拷贝一个元素
    s := arr[0: 1]
    s[0] = 123
    fmt.Println(s) // [123]
    fmt.Println(arr)  // [123 0 0 0 0 1]

    //我们看到对切片的修改是会影响底层数组的
    //如果我们不是手动从已存在的数组拷贝的话,而是使用其他方式创建的话,那么golang会默认给你分配一个底层数组
    //只不过这个数组我们看不到罢了,但它确实是分配了。如果从已经存在的数组中拷贝,那么这个数组就是拷贝出来的切片的底层数组

    //并且我们拷贝切片的时候,还可以指定容量
    //注意:第三个6可不是步长,而是用来指定容量的,但它又不完全等于容量
    s1 := arr[2:4:6]
    //arr[start:end:cap],此时cap-start才是容量,所以这里的容量是6-2=4
    //所以这里的cap无论何时都不能超过底层数组的元素个数
    fmt.Println(len(s1), cap(s1)) //2 4
}

slice的截取

我们如果使用make创建、或者直接声明切片的话,那么会默认给我们创建一个底层数组。但是问题就在于,我们很多时候是会从数组中拷贝的,而这里面会隐藏着一些玄机。

package main

import "fmt"

func main() {
    //此时数组共有8个元素,元素的最大索引为7
    var arr = [...]int{0, 1, 2, 3, 4, 5, 6, 7}

    //此时s1和s2都指向了arr,只不过它们指向了不同的部分。
    //我们看到s1的第一个元素,就是s2的第二个元素
    s1 := arr[1: 2]
    s2 := arr[0: 2]

    s2[1] = 111
    //首先s2就不需要看了,但是我们看到s1也被改了,而且底层数组也被改了
    fmt.Println(s1) //[111]
    fmt.Println(arr) // [0 111 2 3 4 5 6 7]
    
    //很好理解,因为我们可以把切片看成是底层数组的一个映射,修改切片等价于修改底层数组
    //s1和s2映射同一个底层数组
}

上面的很好理解,然后我们再来看看下面的例子

package main

import "fmt"

func main() {
    var arr = [...]int{0, 1, 2, 3, 4, 5, 6, 7}
    s1 := arr[1: 2]
    fmt.Println(s1[3: 6]) // [4 5 6]

    defer func() {
        if err := recover(); err != nil {
            fmt.Println(err)
        }
    }()
    fmt.Println(s1[3])
    // runtime error: index out of range
}

惊了,s1里面只有一个元素,我们居然能够通过s1[3: 6]访问,而且后面我们访问s1[3]明明报错了啊。所以这就是切片的可扩展性,其实我们上面的图画的不是很准确。

因此切片实际上是可扩展的,如果对切片进行索引的话,那么最大索引就是切片的大小减去1。但是如果对进行切片的话(reslice),那么是根据底层数组来的。我们看到s[3:6]对应底层数组是[4, 5, 6],所以是不会报错的。尽管s1只有一个元素,但是它记得自己的底层数组,并且是可扩展的。并且这个扩展只能是向后扩展,可以看到后面的底层数组的元素。但是不能向前扩展,比如底层数组的0,通过s1的话是无论如何都获取不到的。

但是如果我们指定了容量呢?

package main

import "fmt"

func main() {
    var arr = [...]int{0, 1, 2, 3, 4, 5, 6, 7}
    //对于从数组截取切片的,s1 := arr[1: 2]等价于s1 := arr[1: 2: len(arr)]
    //表示s1的容量为len(arr) - 1,相当于当前的切片能够从当前位置能够扩展到arr的尽头
    //但是这里我们指定5,表示最多只能扩展4个元素
    s1 := arr[1: 2: 5]
    fmt.Println(s1[2: 4]) // [3 4]

    defer func() {
        if err := recover(); err != nil {
            fmt.Println(err)
        }
    }()
    fmt.Println(s1[3: 5])
    // runtime error: slice bounds out of range
}

我们看到此时访问[2: 4]是可以的,但是访问[3: 5]就报错了,因为我们这里指定了容量。

s1向后扩展最多只能扩展四个元素,那么访问[3: 5]肯定就报错了

另外我们知道,由数组创建两个切片,对任何一个切片进行修改都会影响底层数组吗,进而影响另一个切片。如果我创建了一个切片s1,然后根据s1再创建出s2,那么对s2修改同样会影响底层数组,进而影响s1。因为它们指向的都是同一个底层数组,并且s2依旧是可以向后扩展的,至于能向后扩展多少就根据它的容量来决定了,总之不能超过底层数组。

package main

import "fmt"

func main() {
    var arr = [...]int{0, 1, 2, 3, 4, 5, 6, 7}
    s1 := arr[0: 2]
    s2 := s1[1: 2]
    s2[0] = 123
    //修改s2
    fmt.Println(s1) //[0 123]
    fmt.Println(arr) //[0 123 2 3 4 5 6 7]
    //查看s2的容量,因为arr有8个元素,而s2是从第一个元素获取的,由于没有指定容量,那么容量就是8-1=7
    fmt.Println(cap(s2)) //7
    fmt.Println(s2[: 4]) // [123 2 3 4]

    //创建大小为1,容量为10的切片
    //本质是一样的,但是此时的底层数组就是golang为我们分配的了,我们就看不到了
    s3 := make([]int, 1, 10)
    //通过索引访问,那么最大索引是0,因为只有1个元素
    s3[0] = 111
    //通过[:],或者直接打印s3,那么还是会只获取切片的全部值
    fmt.Println(s3[:]) //[111]
    //如果手动指定结束位置的话,还是会向后扩展,拿到底层数组对应的值的
    fmt.Println(s3[: 5])//[111 0 0 0 0]
}

切片的扩容

切片的扩容,实际上就是对底层数组的扩容,假设我们申请的切片容量是3,那么对应的底层数组的大小就是3。我们知道切片是可以进行append的,如果容量不够的话,怎么办呢?显然就要进行扩容了那么是怎么扩容的呢?

package main

import "fmt"

func main() {
    var s = make([]int, 0, 3)
    s = append(s, 1)
    fmt.Printf("%p\n", &s[0]) //0xc000060140
    s = append(s, 2)
    fmt.Printf("%p\n", &s[0]) //0xc000060140
    s = append(s, 3)
    fmt.Printf("%p\n", &s[0]) //0xc000060140

    //我们知道此时如果再append,那么容量肯定不够了
    s = append(s, 4)
    fmt.Printf("%p\n", &s[0]) //0xc00008c030
}

我们看到扩容之前,s[0]的地址时不变的。但是扩容之后,地址变了。说明golang中切片的扩容是在底层申请一个更大的数组,让s指向这个新的数组,并把对应元素依次拷贝过去(所以&s[0]会变),那么原来var s = make([]int, 0, 3)所指向的底层数组怎么办呢?这个不用担心,golang的垃圾回收机制会自动销毁它

package main

import "fmt"

func main() {
    var arr = []int{1, 2, 3}
    s1 := arr[1: 3]
    s2 := s1[: 2]
    fmt.Println(&s1[0], &s2[0]) //0xc000060148 0xc000060148

    //此时s1和s2都是[2, 3],下面给s2扩容
    s2 = append(s2, 4)
    fmt.Println(&s1[0], &s2[0]) //0xc000060148 0xc000060180
    fmt.Println(s1[0], s2[0]) //2 2
}

惊了,我们看到s2的第一个元素的地址变了,而s1的第一个元素的地址没有变。因此可以猜测扩容之后,s2指向了新的数组,但是s1还是指向了原来的数组。事实上也确实如此,因为对s2扩容,发现底层数组容量不够,那么就申请一个更大的,让s2重新指向,但是s1还是指向原来的底层数组。而且既然s1引用的还是原来的数组的话,那么原来的数组则不会被gc回收了,并且我们再对s1做任何操作都不会影响s2了,因为这两个切片指向的不再是同一个底层数组了。

package main

import "fmt"

func main() {
    var arr = []int{1, 2, 3}
    s1 := arr[1: 3]
    s2 := s1[: 2]

    //对s1操作,此时会影响s2
    s1[0] = 111
    fmt.Println(s2) //[111 3]

    //扩容之后,s2指向新的数组
    s2 = append(s2, 4)
    //再对s1操作,不会影响s2
    s1[0] = 333
    //s2[0]还是之前被影响的111,不会是s1新设置的333
    fmt.Println(s2)  //[111 3 4]
}

而且申请更大的底层数组的时候,并不是把原来的数组全部都拷贝过去,而是把切片对应的底层数组的元素拷贝过去。因为切片无法向前扩展,那么不好意思,前面的元素就不会拷贝了。

切片和数组的区别

slice的底层数据是数组,slice是对数组的一个封装,它描述数组的一个片段,两者都可以通过下表来访问单个元素。

数组是定长的,长度定义好之后不能再更改。在go中,数组不常用,因为长度也是类型的一部分,限制了它的表达能力,比如[3]int[4]int就是不同的类型。

而切片则非常灵活,它可以动态扩容,并且类型和长度无关。

内容参考:码农桃花源

猜你喜欢

转载自www.cnblogs.com/traditional/p/12216947.html