go从零开始2 [slice]

继上一篇map之后,我们继续对slice类型的详细梳理。

正式开始之前,先抛几个问题,

func main() {
	question()
	question1()
	question2()
}

func question() {
	// 问题1
	slice := []int{1, 2, 3, 4}
	mem := slice[1:1]
	fmt.Println(mem)      // 输出什么 结果:[]
	fmt.Printf("%T", mem) // 输出类型是什么 结果:[]int
}

func question1() {
	// 问题2
	slice := []int{1, 2, 3, 4, 5}
	slice2 := append(slice[:3], 6, 7)

	fmt.Println(slice2) // 输出什么 结果:[1 2 3 6 7]
	fmt.Println(slice)  // 输出什么 结果:[1 2 3 6 7]

	newSlice := []int{1, 2, 3, 4, 5}
	newSlice2 := append(slice[:3:3], 6, 7)

	fmt.Println(newSlice2) // 输出什么 结果:[1 2 3 6 7]
	fmt.Println(newSlice)  // 输出什么 结果:[1 2 3 4 5]
}

func question2() {
	fu0 := func(array [5]int) {
		array[0] = 9
		fmt.Printf("fu1 数组值: %v, fu1 数组内存地址:%p \n", array, &array)
	}

	fu1 := func(l []int) {
		l[0] = 9
		fmt.Printf("fu1 slice值: %v, fu1内 slice 内存地址:%p \n", l, l)
	}

	// 问题 3: 切片传值问题
	array := [5]int{1, 2, 3, 4, 5}
	fmt.Printf("数组:%v , 数组内存地址:%p \n", array, &array)
	fu0(array)
	fmt.Printf("数组:%v , 数组内存地址:%p \n\n", array, &array)  //思考为啥不变?

	slice := array[0:]
	fmt.Printf("slice值:%v , slice 内存地址:%p \n", slice, slice)
	fu1(slice)
	fmt.Printf("slice值:%v , slice 内存地址:%p \n", slice, slice) //思考为啥变了?
	fmt.Printf("数组:%v , 数组内存地址:%p \n\n", array, &array)  //what 为啥又变了。。
	
	/* 打印结果
	
	数组:[1 2 3 4 5] , 数组内存地址:0xc00012c030
	fu1 数组值: [9 2 3 4 5], fu1 数组内存地址:0xc00012c090
	数组:[1 2 3 4 5] , 数组内存地址:0xc00012c030

	slice值:[1 2 3 4 5] , slice 内存地址:0xc00012c030
	fu1 slice值: [9 2 3 4 5], fu1内 slice 内存地址:0xc00012c030
	slice值:[9 2 3 4 5] , slice 内存地址:0xc00012c030
	数组:[9 2 3 4 5] , 数组内存地址:0xc00012c030
	
	*/
}

上面的问题如果你们都懂了,恭喜你完全理解了slice~
不懂的盆友,可以继续往下看

要理解的slice,首先要先理解array。那么在go中array是一个什么样的存在呢? 其实array就是go在底层中开辟的一个连续的内存空间,那么切片(slice)就是对这串连续空间的引用。有的兄弟会说,哎呀老哥,你这说的有点抽象呀,我还是不太理解,咋滴搞嘛。。 嗯。。那我们就看看slice的底层结构,再来理解一下。

// slice的底层结构
type slice struct {
	array unsafe.Pointer  //嗨,胸弟,这就是~ 这是个指针,指向了底层的数组
	len   int  // 切片的长度
	cap   int  // 切片的容量
}

现在知道切片到底是个什么东西了。我们来看看切片是如何创建
要么从数组或者切片去切分或者自己声明去创建。

//声明创建slice
var slice []int
var slice = []int{}
var slice []int = nil
slice := make([]int, len, cap)

//通过截取的方式
array := [...]int{1,2,3,4,5}

//start为起始下标  len位置为空,则自动使用截取切片或数组的长度。cap位置为空,则自动使用截取切片或数组的容量。
//slice 的 len(slice) = len-start 
//slice 的 cap(slice) = cap-start
slice := array[start : len : cap] 

说了这么多。。所以问题一的总结来了。

  1. 获取切片元素时,下标最大值为len(array)-1。
  2. 截取切片时,起始下标可为len(array),len最大为array的长度,cap最大为array的容量。起始下标为len(array) 返回array类型的空值。其他情况系统报错。

都到这里有没有理解切片引用底层数组的逻辑呢。。。 (正在看书的小涛说:没有!!!)我们继续~

那我们解析一下上述代码。

初始化一个切片
slice := []int{1, 2, 3, 4, 5}

slice2 := append(slice[:3], 6, 7)  这一步是对切分出来的切片进行元素的添加,拆分下
tmp := slice[:3]  这一步实际的操作是 slice[0:3:5]  那得到的是什么结果呢?
值的结果为:[1,2,3] 但是因为容量为5,模拟下值:[1,2,3, $1,  $2]
$i相当于是slice[3]的内存地址,$2相当于slice[4]的内存地址。
所以 append添加6,7后。导致slice的值发生了变化。

文字描述都是浮云,我们上图为证

image

图中所示 len(months) = 13 cap(months) = 13。Q2 = months[4:7] 因为截取时,cap为空,根据上述的规则 cap(Q2) = cap(months)-start 即 cap(Q2) = 13 - 4 = 9 。
同理 summer[6:9] , cap(summer) = cap(months)-start 即 cap(summer) = 13 - 6 = 7

继续探讨我们的问题2

slice := []int{1, 2, 3, 4, 5}
tmp := slice[:3:4]
tmp = append(tmp, 6)
tmp = append(tmp, 7)

这个时候tmp和slice分别又是什么呢?为什么?
我们先思考两分钟,然后我来公布答案。。

叮叮叮~ 两分钟到啦~

slice = [1,2,3,6,5]
tmp = [1,2,3,6,7]
为什么会这样呢?
原因是tmp的容量为4,在添加6的时候,tmp最后的位置和slice是同一块地址,所以同时更新为6,tmp继续添加7的时候,因为tmp容量不够了,需要进行扩容。扩容操作会导致底层数组的变更,所以slice中的5 不会改变。切片的扩容符合一下规则:

  1. 切片容量小于1024时,倍增扩容
  2. 切片容量不小于1024时,按1.25倍扩容。

来,让我们检验一下。上~ 代码~

func testLenCap() {
	var sliceTest []int
	fmt.Printf("init: len=%d cap=%d \n", len(sliceTest), cap(sliceTest))
	for i := 1; i < 10000; i++ {
		innerCap := cap(sliceTest)
		innerPro := float32(0)
		sliceTest = append(sliceTest, i)
		if innerCap != cap(sliceTest) {
			if innerCap != 0 {
				innerPro = float32(cap(sliceTest)) / float32(innerCap)
			}
			fmt.Printf("init: len=%d cap=%d rate=%f \n", len(sliceTest), cap(sliceTest), innerPro)
		}
	}
}

为了便于观察,我们只在容量变化时,进行打印,打印结果如下:
init: len=0 cap=0
change: len=1 cap=1 rate=0.000000
change: len=2 cap=2 rate=2.000000
change: len=3 cap=4 rate=2.000000
change: len=5 cap=8 rate=2.000000
change: len=9 cap=16 rate=2.000000
change: len=17 cap=32 rate=2.000000
change: len=33 cap=64 rate=2.000000
change: len=65 cap=128 rate=2.000000
change: len=129 cap=256 rate=2.000000
change: len=257 cap=512 rate=2.000000
change: len=513 cap=1024 rate=2.000000
change: len=1025 cap=1280 rate=1.250000
change: len=1281 cap=1696 rate=1.325000
change: len=1697 cap=2304 rate=1.358491
change: len=2305 cap=3072 rate=1.333333
change: len=3073 cap=4096 rate=1.333333
change: len=4097 cap=5120 rate=1.250000
change: len=5121 cap=7168 rate=1.400000
change: len=7169 cap=9216 rate=1.285714
change: len=9217 cap=12288 rate=1.333333

相信看到这里你已经了解了切片和数组之前的爱恨情仇。那我们继续补充一点,当数组和切片作为参数时的场景,也就是问题3
首先明确一点,那就是函数的参数是以值拷贝的形式传递的。
首先传递的数组,在函数体内修改数组,发现数组值没有发生变化,且根据打印的地址,确实不一致。
第二次调用 传切片,可以发现外面的切片的值发生了变化,且根据打印的内存地址发现,函数内和函数外是相同的内存地址,所以对待切片要深刻理解 :切片是底层数组引用。还有就是在向函数传值时传递的是引用,函数仍然也是值拷贝,只不过是对引用的拷贝罢了,两个值同时指向一个地址。

好了~~ 到这里基本把切片剥好了~ 就等大家使用啦

文章如有错误,请各位大佬不吝指教~

posted @ 2022-12-16 19:47  丢弃西瓜的小涛  阅读(50)  评论(0编辑  收藏  举报

欢迎