Go 切片在做参数传递时,是值传递还是引用传递?

2,149 阅读5分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 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)(大佬写的很详细,有兴趣大家可以细品一下)