Golang的Slice重要小知识Ⅱ

关于切片作为函数参数进行传递的问题

昨天,在做一道力扣题的时候,小编使用切片作为函数参数进行传递,并在函数中对切片进行修改,在小编印象里,切片属于引用类型,所以它传参数应该是引用传递,所以实参应该也是会有同样的变化,于是自信满满的提交了,可是结果出乎意料,并非小编所想的那样,那就让我们开启今天的话题,讨论讨论切片slice作为函数参数,是值传递还是引用传递?(感觉这完全可以是一道面试题!考验理论基础的时候到了!)

一、值传递-指针传递-引用传递

什么是值传递?值传递就是传递参数的时候,将实参拷贝一份作为形参在传递到函数中使用,实参与形参所指向的地址空间是不同的,所以在函数中修改形参,实参是不会有相同的变化的。
看示例代码:

func main() {
	num := 10
	testnum(num)
	fmt.Printf("main num:%v\n", num)
}
func testnum(num int) {
	fmt.Printf("testnum num:%v\n", num)
	num = 5
	fmt.Printf("testnum num:%v\n", num)
}
输出结果:
testnum num:10
testnum num:5
main num:10

可见即使在testnum函数中改变了这个参数,但是main中的num是不会改变的,因为他们不是同一块数据空间,testnum的参数num是main的num的拷贝。
什么是指针传递?指针传递就是声明一个指针,指针的值是一个地址,该地址一个指向一块数据空间,当用指针作为参数进行传递的时候,那么就可以实现在函数中修改main中实参的数据了。
看示例代码:

func main() {
	num := 10
	p := &num
	testpoint(p)
	fmt.Printf("main p address:%p\n", &p)
	fmt.Printf("main num address:%v\n", p)
	fmt.Printf("main num:%v\n", num)
}
func testpoint(tp *int) {
	fmt.Printf("testpoint p address:%p\n", &tp)
	fmt.Printf("testpoint num address:%v\n", tp)
	fmt.Printf("testpoint oldnum :%v\n", *tp)
	*tp = 5
	fmt.Printf("testpoint newnum :%v\n", *tp)
}
输出结果:
testpoint p address:0xc000006030
testpoint num address:0xc000012090
testpoint oldnum :10
testpoint newnum :5
main p address:0xc000006028
main num address:0xc000012090
main num:5

上述代码可知:我们声明了一个指针p指向num的数据空间,也就是p的值是num的空间地址0xc000012090,然后将指针作为参数传递的时候,在函数中tp参数的也是指向同一空间地址,所以对函数tp参数所指向的空间的数据进行修改时,main中p指针所指的数据空间是同一块,所以会发生同样的修改,所以才会有如下的结果。

开始进入今天的主题
什么是引用传递?在golang中,slice切片是属于引用类型,与指针类型有点类似,但是slice切片是一个结构体struct,然后该结构体有三个字段,分别是底层数组的引用,容量cap和长度len,为什么它是引用类型,因为它本身并不是一个基本数据类型数组,而是一个指向底层数组的一个数据类型,所以它是引用类型,那么将它作为参数传递的时候会发生什么?我们看看代码:

func main() {
	var arr []int = []int{1, 2, 3}
	testslice(arr)
	fmt.Printf("main arr address:%p\n", &arr)
	fmt.Printf("main *arr address:%p\n", arr)
	fmt.Println("main:", arr, len(arr), cap(arr))
}

func testslice(tarr []int) {
	tarr[1] = 0
	fmt.Printf("testslice tarr address:%p\n", &tarr)
	fmt.Printf("testslice *tarr address:%p\n", tarr)
	fmt.Println("testslice:", tarr, len(tarr), cap(tarr))

}
输出结果:
testslice tarr address:0xc0000044a0
testslice *tarr address:0xc000010420
testslice: [1 0 3] 3 3
main arr address:0xc000004480
main *arr address:0xc000010420
main: [1 0 3] 3 3

可见,当我们将切片作为参数进行传递的时候,并在函数中修改切片中的某一个元素,main中实参切片也会发生同样的改变,那是因为arr切片和tarr切片所指向的底层数组是同一个,是0xc000010420这块空间,所以某一切片在同一个底层数组做修改,另一个切片肯定也会有同样的变化。

二、为什么切片作为参数传递实际是值传递?

看了上面的代码,想必大伙肯定能发现,其实arr切片和tarr切片两者本身的地址是不同的,说明什么?两者是不同的切片,并非同一个切片,除了切片的地址不同,第二个示例的指针地址也是不同的,有没有发现?虽然他们所指向的数据空间的地址相同,但是每个数据空间都有自己的一个地址,它两同样是不同指针,所以引发思考?
指针传递和引用传递看似简单的只是把同一块数据空间进行传递,不做拷贝,以至于可以做到函数内修改main变量的值。但实际上不是,先看一段关于切片做为参数的代码:

func main() {
	var arr []int = []int{1, 2, 3}
	testslice(arr)
	fmt.Println("main:", arr, len(arr), cap(arr))
	fmt.Printf("main *arr address %p\n", arr)
}

func testslice(tarr []int) {
	tarr = append(tarr, 4)
	fmt.Println("testslice:", tarr, len(tarr), cap(tarr))
	fmt.Printf("testslice *arr address %p\n", tarr)

}
输出结果:
testslice: [1 2 3 4] 4 6
testslice *arr address 0xc0000b6060
main: [1 2 3] 3 3
main *arr address 0xc0000a0120

可见,testslice无法改变main中的切片arr,这时候小伙伴很清楚的回答:“因为testslice中,tarr因为append发生了扩容,由于扩容是要用一个新的更大的底层数组,那么肯定不是指向同一块底层数组了,结果当然改变不了main中的切片”。确实!这个示例是这个道理,我们从arr所引用的底层数组的地址就可以发现是不同底层数组,那如果是下面这一段代码呢?容量足够的情况下,不会发生扩容的情况下呢?

func main() {
	arr := make([]int, 3, 10)
	testslice(arr)
	fmt.Println("main:", arr, len(arr), cap(arr))
	fmt.Printf("main *arr address %p\n", arr)
}
func testslice(tarr []int) {
	tarr = append(tarr, 1)
	fmt.Println("testslice:", tarr, len(tarr), cap(tarr))
	fmt.Printf("testslice *arr address %p\n", tarr)
}
输出结果:
testslice: [0 0 0 1] 4 10
testslice *arr address 0xc00000e230
main: [0 0 0] 3 10
main *arr address 0xc00000e230

可见,两切片仍然指向同一个底层数组0xc00000e230,说明没有变更底层数组,那为什么main和testslice的切片输出的结果为什么不一样呢?这话题在理论基础还不扎实的同学里讨论的喋喋不休。这就是今天的重点,其实Golang中所有的参数传递都是值传递,包括切片。在对切片作为参数进行传递的时候,其实做了这些事,新建一个形参切片,然后把实参切片的三个字段对应的值拷贝给了形参切片,所有两个切片是除了自身地址不一样,其他都是一样的(相当于拷贝了一个切片作为参数)。那么当testslice在不发生扩容的情况下,append一个元素时形参切片的len改变了扩大了,但实参的切片的len没有变化,所有按照实参切片进行输出,只能输出到len之前,但实际上len后面是有testslice进行append的那个元素的,如下看测试:

func main() {
	arr := make([]int, 3, 10)
	testslice(arr)
	fmt.Println("main:", arr, len(arr), cap(arr))
	fmt.Printf("main *arr address %p\n", arr)
	//添加一行测试,读取到arr后一个元素
	fmt.Println("main:", arr[:4], len(arr[:4]), cap(arr[:4]))
}

func testslice(tarr []int) {
	tarr = append(tarr, 1)
	fmt.Println("testslice:", tarr, len(tarr), cap(tarr))
	fmt.Printf("testslice *arr address %p\n", tarr)
}
输出结果:
testslice: [0 0 0 1] 4 10
testslice *arr address 0xc00000e230
main: [0 0 0] 3 10
main *arr address 0xc00000e230
main: [0 0 0 1] 4 10

这下子就清晰了,其实append后就是形参的切片len增大了,但是实参的len没有改变,因为是进行切片拷贝后的不同切片,所有才会有不同的输出结果。所有的结果总结出一句话:Golang中所有的参数传递都是值传递,包括切片

今天就到这里的,要坚持每天学习喔,拜~

你可能感兴趣的:(golang核心编程,golang)