引用类型≠传引用

Golang slice

Posted by JC on November 4, 2020

切片传参幻觉:传引用

golang中函数的参数为切片时是传引用还是传值?对于这个问题,当你百度一轮过后,你会发现很大一部分人认为是传引用,通常他们会贴出下面这段代码进行佐证

pacakge main

func changeSlice(s []int) {
    s[1] = 111
}

func main() {
    slice := []int{0, 1, 2, 3}
    fmt.Printf("slice: %v \n", slice)

    changeSlice(slice)
    fmt.Printf("slice: %v\n", slice)
}

从输出结果我们看到,函数changeSlice内对切片的修改,main函数中的切片变量slice也跟着修改了。咋一看,这不就是引用传递的表现吗?

三个概念

  • 传值
  • 传指针
  • 传引用

传值没什么好说的,传指针和传引用还是有区别的

传指针时,值虽然相同,但是存放这两个指针的内存地址是不同的,因此这是两个不同的指针。

任何存放在内存里的东西都有自己的地址,指针也不例外,它虽然指向别的数据,但是也有存放该指针的内存。

传引用时,将实际参数的地址传递到函数中,在函数中对参数所进行的修改,将影响实际参数。

但是,在Go中无法举例说明传引用

官方:Go函数传参只有值传递

Slice结构体

type slice struct {
    array unsafe.Pointer
    len   int
    cap   int
}

type Pointer *ArbitraryType

第一部分是指向底层数组的指针,其次是切片的大小len和切片的容量cap。(果然含有指针成员变量。)

分析

Go里面函数传参只有值传递一种方式,所以当切片作为参数时,其实也是切片的拷贝,但是在拷贝的切片中,其包含的指针成员变量的值是一样的,也就是说它们指向的数据源是一样,因此在调用函数内修改形参能影响实参。

于是我们又可以这样总结:
Go语言中所有的传参都是值传递(传值),都是一个副本,一个拷贝。因为拷贝的内容有时候是非引用类型(int、string、struct等这些),这样就在函数中就无法修改原内容数据;有的是引用类型(指针、map、slice、chan等这些),这样就可以修改原内容数据。

这里要注意的是:引用类型和传引用是两个概念。

切片参数,修改形参一定影响实参吗

前些天遇到这样的问题

func main(){
	s := make([]int, 0)
	fmt.Println(len(s))
	AlterSlice(s)
	fmt.Println(len(s))
}

func AlterSlice(s []int){
	s = append(s, 1)
	s = append(s, 2)
	fmt.Println(len(s))
}

期望在AlterSlice之后输出长度为2,可实际打印的长度为0,这又是怎么回事?

函数内修改形参s后,外部的实参slice并没有跟着改变,而且注意到一点是,通过地址打印,发现形参和实参的指针成员变量的值是不一样的,也就是说它们指向的数据源不是同一个了。

这又是为什么呢?

扩容

当需要扩容时,append会做哪些操作呢?

  1. 创建一个新的临时切片t,t的长度和slice切片的长度一样,但是t的容量是slice切片的2倍,新建切片的时候,底层也创建了一个匿名的数组,数组的长度和切片容量一样。
  2. 复制slice里面的元素到t里,即填入匿名数组中。然后把t赋值给slice,现在slice的指向了底层的匿名数组。
  3. 转变成小于容量的append方法。

举个例子,数组arr = [3]int{0, 11, 22},生成一个切片slice := arr[1:3],使用append方法往切片slice中追加元素33,将发生以下操作:

再回到刚刚举的例子,之所外部的实参切片变量slice不受形参切片变量s修改的影响,因为在执行完 s = append(s, 1) 这段代码后,切片s的指针指向的数组发生了扩容,其指针指向了新的数组,因此当再次修改其第二个元素的值时,是不会影响外部切片变量slice的。