zl程序教程

您现在的位置是:首页 >  后端

当前栏目

Golang-绕不开的数组和切片总结

数组Golang 总结 切片
2023-06-13 09:16:56 时间

前言

Go语言中slice和数组是非常像的两种数据结构,但是切片(slice)比数组更好用,Go更推荐slice。当然在面试中也是出现频率非常高的,总结一些数组和slice常见的问题。

1.数组与切片的区别

切片(slice)结构的本质对数组的封装,都可以通过下标来访问单个元素。 数组是定长,定义好长度就不能再改变,不同的长度代表不同的类型 数组是一片连续的内存 切片可以动态扩容,类型与长度无关 切片实际上是一个结构体,包含三个字段:长度、容量,底层数组

//数组
array := [3]int{1, 2, 3}
//切片
slice := []int{1, 2, 3}

//切片底层结构
//src/runtime/slice.go
type slice struct {
	array unsafe.Pointer //指向底层数组
	len int	//长度
	cap int //容量
}

2.nil slice和空 slice的区别

nil slice:声明为切片,但是没有分配内存,切片的指针是nil。

//只有声明的切片才会产生nil切片,而且还没有分配内存
var slice []int
var slice = *new([]int)

空 slice:切片指针指向了一个数组内存地址,但是数组是空的, 空切片有两种方式产生。

//len和cap都为0
s1 := []int{}   //1.空切片,没有任何元素
 
s2 := make( []int, 0)  //2.make 切片,没有任何元素

3.切片是如何截取的

截取是创建切片的一种常见方式,可以从数据或者slice直接截取,需要指定起止索引位置。 基于已有的数组或者slice进行创建新的slice对象时。新slice和老slice共用底层数据,因此对底层数组的更改都会影响到彼此。前提是两者共用底层数组,如果因为执行append操作使得slice的底层数组发生了扩容,两者就就不会相互影响了,关键点就是是否共用了底层数组

截取的方式:

data := [...]int{1,2,3,4,5}
//1:low(开始位置) 2:high(结束位置) 4:max(容量)
slice := data[1:2:4]

那么low、high、max之间有怎么样的关系呢?首先data可以是slice也可以是数组。low是低索引值,是闭区间,也就是第一个元素的索引下标位置是low:而high则是开区间,什么是开区间呢,就是不包含这个值,比如最后一个元素只能是high-1出的下标元素值。而容量大小为max-1。 满足以下关系:

  1. max >= high >= low
  2. high == low时,新slice为空
  3. high和max需要在原始的数组或者slice容量范围内

4.内建函数make和new的区别

make和new的函数形式如下

func make(t Type, size ...IntegerType) Type

func new(Type) *Type

相同点:Go语言用来分配内存的函数 不同点: 1:适用的类型不同:make适用于给slice、map、channel分配内存,new适用于int类型、数组、结构体等值类型 2:返回类型不同:make返回一个值,new返回一个指向变量指针 3:make分配空间后会进行初始化, new分配的空间会被清零

//make创建一个长度为0,容量为5的int类型切片
s := make([]int, 0 ,5)
//new分配一个零值得int型
a = new(int)
*a = 5

5.切片作为函数参数会被改变吗

我们知道Go的切片(slice)是引用类型的值,它有以下一些特性。

  • Go的slice类型中包含了一个array指针以及len和cap两个int类型的成员
  • Go中的参数传递实际都是值传递,将slice作为参数传递时,函数中会创建一个slice参数的副本,这个副本同样也包含array,len,cap这三个成员
  • 副本中的array指针与原slice指向同一个地址,所以当修改副本slice的元素时,原slice的元素值也会被修改。但是如果修改的是副本slice的len和cap时,原slice的len和cap仍保持不变
  • 如果在操作副本时由于扩容操作导致重新分配了副本slice的array内存地址,那么之后对副本slice的操作则完全无法影响到原slice,包括slice中的元素

通过几个场景来感受下不同情况下slice作为函数参数值的变化。

5.1:在函数中修改slice的成员值:

slice1 := []int{1, 2, 3, 4, 5}
	fmt.Printf("slice1: %v\n", slice1)
	fmt.Printf("address of slice1: %p    %p\n", slice1, &slice1)

	// 重置数据
	resetSlice(slice1)
	fmt.Printf("slice1 after reset: %v\n", slice1)
	fmt.Printf("address of slice1 after reset: %p    %p\n", slice1, &slice1)
	
	//重置函数
	func resetSlice(slice2 []int) {
	    for i := 0; i < len(slice2); i++ {
		    slice2[i] = slice2[i] + i*10
	    }
	    fmt.Printf("address of weight: %p      %p\n\n", slice2, &slice2)
    }

上面的输出结果:

slice1: [1 2 3 4 5]
address of slice1: 0xc000088630    0xc000094860

address of slice2: 0xc000088630      0xc0000948c0
slice2 after reset: [1 12 23 34 45]

slice1 after reset: [1 12 23 34 45]
address of slice1 after reset: 0xc000088630    0xc000094860

可以看出函数中修改了slice2的值,原来的slice1的值也改变了。因为函数参数传递时,实际上传递的是引用指向的地址。函数实际上另外开辟了一个临时变量weight来存放这个引用的值,新变量的地址是0xc0000948c0。而实际修改的是slice2指向的地址0xc000088630存放的值,所以可以看到slice1也改变了。

5.2:在函数中向slice进行append新成员:

myWeight := make([]int, 1, 3)
	fmt.Printf("myWeight: %v\n", myWeight)
	fmt.Printf("address of myWeight: %p       %p\n", myWeight, &myWeight)
	fmt.Printf("myWeight len: %v, cap: %v\n\n", len(myWeight), cap(myWeight))

	// 添加数据
	addWeightRecord(myWeight)
	fmt.Printf("myWeight after add: %v\n", myWeight)
	fmt.Printf("address of myWeight after add: %p     %p\n", myWeight, &myWeight)
	fmt.Printf("myWeight len: %v, cap: %v\n", len(myWeight), cap(myWeight))
	
	//添加数据函数
	func addWeightRecord(weight []int) {
    	weightCap := cap(weight)
	    weight[0] = 10
	    fmt.Printf("cap of weight: %v\n\n", weightCap)
	    for i := 0; i < weightCap-1; i++ {
		    weight = append(weight, i)
	    }

	    fmt.Printf("weight: %v\n", weight)
	    fmt.Printf("address of weight: %p     %p\n", weight, &weight)
	    fmt.Printf("weight len: %v, cap: %v\n", len(weight), cap(weight))
    }

上面输出结果

myWeight: [0]
address of myWeight: 0xc0001acaa0       0xc0001a68c0
myWeight len: 1, cap: 3

cap of weight: 3

weight: [10 0 1]
address of weight: 0xc0001acaa0     0xc0001a6920
weight len: 3, cap: 3

myWeight after add: [10]
address of myWeight after add: 0xc0001acaa0     0xc0001a68c0
myWeight len: 1, cap: 3

先看现象:函数中修改slice中的变量值后,外部slice也同样进行了修改。但是向slice中append添加元素时,外部的slice并未进行添加元素,那么这又是什么原因导致的呢? 我们知道slice的底层是由指向数组的指针、len、cap组成的结构体,对第一个元素的修改,内外的slice的第一个元素值都是10,因为内外slice都是指向的slice的地址0xc0001acaa0。但是append的时候weight的len会变化为3,cap不会变(未超过容量)。但是myWeight的len不会变,所以只能读到第一个元素值10。

5.3:在函数中向slice进行append新成员,并超出cap:

接着场景2,如果append对应的循环weightCap-1改成6,会发现函数内部出现了扩容。slice中对应的array指向的地址会发生变化,是两个不同的slice.

6.切片的容量增长

slice切片的扩容对于append向slice添加元素时,假如容量cap够用,追加新元素进去,slice的len会增加。如果容量不够,则slice先进行扩容得到新的slice,然后将元素追加到新slice。

扩容规则

当切片slice容量小于1024时,将以两倍速度进行扩容,也就是新的slice容量将会是原slice容量的两倍。当切片容量较大时(原slice的容量大于等于1024),为了避免空间浪费,将采用较小的扩容倍速(新的扩容将大于等于原来1.25倍)。其实很多网上的总结是1.25倍,但是在实际扩容时需要考虑内存对齐,所以扩容时大于等于1.25倍的。