开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第2天,点击查看活动详情
前言
在Go语言中,值类型和引用类型有以下特点:
- 「值类型」:基本数据类型,int,float,bool,string,以及数组和struct 特点:变量直接存储值,内存通常在
栈上分配,栈在函数调用完会被释放 - 「引用类型」:指针,slice,map,chan,interface等都是引用类型 特点:变量存储的是一个地址,这个地址存储最终的值。内存通常在
堆上分配,通过GC回收
但是当我将切片作为函数参数进行传递时,出现了离谱的事情
//这里是通过回溯递归记录二叉树中每条路径节点值的和,通过二维数组返回满足条件的路径
func pathSum(root *TreeNode, target int) [][]int {
if root == nil {
return [][]int{}
}
temp := [][]int{}
cur := []int{}
traveral(root, temp, cur, target)
return temp
}
func traveral(root *TreeNode, temp [][]int, cur []int, target int) {
cur = append(cur, root.Val)
if root.Left == nil && root.Right == nil {
if target - root.Val == 0 {
tmp := make([]int, len(cur))
copy(tmp, cur)
temp = append(temp, tmp)
}
return
}
if root.Left != nil {
traveral(root.Left, temp, cur, target-root.Val)
cur = cur[:len(cur)-1]
}
if root.Right != nil {
traveral(root.Right, temp, cur, target-root.Val)
cur = cur[:len(cur)-1]
}
}
//output:[]
最后返回的二维数组是一个空的切片,但是当我将函数参数改成切片的指针进行传递时,返回正确。这让我好奇,我们都知道切片是引用类型,但是为什么在当参数传递时出现意外?
正文
首先,我们要明确三个概念:
- 传值(值传递)
- 传指针
- 传引用(引用传递)
传值(值传递)
是指在调用函数时将实际参数拷贝一份传递到函数中,这样在函数中对参数进行修改不会影响到实际参数。
传指针
形参是指向实参地址的指针,当对形参的指向进行操作时,就相当于对实参本身进行操作
func main() {
a := 10
pa := &a
fmt.Printf("value: %p\n", pa)
fmt.Printf("addr: %p\n", &pa)
modify(pa)
fmt.Println("a 的值被修改了,新值为:", a)
}
func modify(p *int) {
fmt.Printf("函数内的 value: %p\n", p)
fmt.Printf("函数内的 addr: %p\n", &p)
*p = 1
}
/*
value: 0xc000016088
addr: 0xc00000e028
函数内的 value: 0xc000016088
函数内的 addr: 0xc00000e038
a 的值被修改了,新值为: 1
*/
从输出结果中我们可以看到,这是一个指针的拷贝。指针pa 和 p 的值虽然相同,但是存放这两个指针的内存地址是不同的,因此这是两个不同的指针。
传引用(引用传递)
是指在调用函数时将实际参数的地址传递到函数中,在函数中对参数所进行的修改,将影响实际参数。以上面demo为例子,如果在modify函数中打印指针变量p的地址也是0xc00000e028,那么我们就认为是引用传递
高潮来了
先看一个小demo
func main() {
arr := []int{1, 2, 3}
fmt.Printf("start: %p, %v\n", &arr, arr)
test(arr)
fmt.Printf("end: %p, %v", &arr, arr)
}
func test(tmp []int) {
tmp[0] = 111
fmt.Printf("modify: %p, %v\n", &tmp, tmp)
}
//output:
// start: 0xc000096060, [1 2 3]
// modify: 0xc000096090, [111 2 3]
// end: 0xc000096060, [111 2 3]
通过结果我们可以看到,切片作为引用类型,确实是我们在函数内对切片值进行修改时,main函数中的切片值也发生了变化,这不就是引用传递的表现吗?
但是我们发现原切片的地址和函数中切片的地址是不一样的,如果函数切片参数传的是引用,那么main打印的切片地址应该是和test函数切片的地址是一样的,因此我们可以确定的说,go函数中切片参数是传引用的说法是错误的
从上面的分析来看,切片参数传递的方式并非是传引用。反而极有可能是传指针,而在传指针那小节的分析中我们可以知道,传指针其实就是指针的拷贝,形参和实参是两个不同的指针,但是它们的值是一样的。本质上可以说还是传值
func main() {
arr := [5]int{0, 1, 2, 3, 4}
slice1 := arr[1:4]
slice2 := arr[2:5]
fmt.Printf("arr %v, slice1 %v, slice2 %v arr addr: %p, slice1 addr: %p, slice2 addr: %p\n", arr, slice1, slice2, &arr, &slice1, &slice2)
fmt.Printf("arr[2] addr: %p, slice1[1] addr: %p, slice2[0] addr: %p\n", &arr[2], &slice1[1], &slice2[0])
arr[2] = 2222
fmt.Printf("arr: %v, slice1: %v, slice2: %v\n", arr, slice1, slice2)
}
//arr [0 1 2 3 4], slice1 [1 2 3], slice2 [2 3 4] arr addr: 0xc000014090, slice1 addr: 0xc00000c080, slice2 addr: 0xc00000c0a0
//arr[2] addr: 0xc0000140a0, slice1[1] addr: 0xc0000140a0, slice2[0] addr: 0xc0000140a0
//arr: [0 1 2222 3 4], slice1: [1 2222 3], slice2: [2222 3 4]
从打印出来的结果可以看到,我们创建出来的切片,各自拥有不同的地址,但是其切片元素拥有着相同的地址,说明这些切片共享着数组arr中的数据,当我们修改arr中元素的值时,两个切片都发生了变化,这也印证了共享着arr中的数据这一点
总结
Go语言中所有的传参都是值传递(传值),都是一个副本,一个拷贝。因为拷贝的内容有时候是非引用类型(int、string、struct等这些),这样就在函数中就无法修改原内容数据;有的是引用类型(指针、map、slice、chan等这些),这样就可以修改原内容数据。
这里要注意的是:引用类型和引用传递是两个概念
参考:(正经版)面试官:切片作为函数参数是传值还是传引用? - 掘金 (juejin.cn)(大佬写的很详细,有兴趣大家可以细品一下)