zoukankan      html  css  js  c++  java
  • 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
    ", &s[0]) //0xc000060140
    	s = append(s, 2)
    	fmt.Printf("%p
    ", &s[0]) //0xc000060140
    	s = append(s, 3)
    	fmt.Printf("%p
    ", &s[0]) //0xc000060140
    
    	//我们知道此时如果再append,那么容量肯定不够了
    	s = append(s, 4)
    	fmt.Printf("%p
    ", &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就是不同的类型。

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

    内容参考:码农桃花源

  • 相关阅读:
    将课程中的所有动手动脑的问题以及课后实验性的问题,整理成一篇文档,以博客形式发表在博客园
    将课程中的所有动手动脑的问题以及课后实验性的问题,整理成一篇文档
    课堂作业 异常与处理
    课堂作业05继承与多态
    课后作业04
    课堂作业03程序设计
    课堂作业03动手动脑问题
    课堂作业02程序设计作业
    课堂作业02动手动脑的问题
    课堂中所有的问题及其课后实验性的问题的文档
  • 原文地址:https://www.cnblogs.com/traditional/p/12216947.html
Copyright © 2011-2022 走看看