Golang中函数传参存在引用传递吗?

Posted hel胡说

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Golang中函数传参存在引用传递吗?相关的知识,希望对你有一定的参考价值。

继后,继续来探讨下面的几个问题:

  1. 函数传参中值传递、指针传递与引用传递到底有什么不一样?

  2. 为什么说 slice、map、channel 是引用类型?

  3. Go中 slice 在传入函数时到底是不是引用传递?如果不是,在函数内为什么能修改其值?

官方文档已经明确说明:Go里边函数传参只有值传递一种方式,为了加强自己的理解,再来把每种传参方式进行一次梳理。

值传递

值传递是指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。

概念总给人一种教科书的感觉,写点代码验证下。

func main() {
    a := 10
    fmt.Printf("%#v\n", &a) // (*int)(0xc420018080)
    vFoo(a)
}

func vFoo(b int) {    fmt.Printf("%#v\n", &b) // (*int)(0xc420018090)
}

注释内容是我机器的输出,你如果运行会得到不一样的输出

根据代码来解释下,所谓的值传递就是:实参 a 在传递给函数 vFoo 的形参 b 后,在 vFoo 的内部,b 会被当作局部变量在栈上分配空间,并且完全拷贝 a 的值。

图中左侧是还未调用时,内存的分配,右侧是调用函数后内存分别分配的变量。这里需要注意,就算vFoo的参数名字是a,实参与形参也分别有自己的内存空间,因为参数的名字仅仅是给程序员看的,上篇文章已经说清楚了。

指针传递

是不是云里雾里的?还是通过代码结合来分析所谓的指针传递。

func main() {
    a := 10
    pa := &a
    fmt.Printf("value: %#v\n", pa) // value: (*int)(0xc420080008)
    fmt.Printf("addr: %#v\n", &pa) // addr: (**int)(0xc420088018)
    pFoo(pa)
}

func pFoo(p * int) {    fmt.Printf("value: %#v\n", p) // value: (*int)(0xc420080008)    fmt.Printf("addr: %#v\n", &p) // addr: (**int)(0xc420088028)
}

引用传递

由于 Go 里边并不存在引用传递,我们常常看到说 Go 中的引用传递也是针对:Slice、Map、Channel 这几种类型(这是个错误观点),因此为了解释清楚引用传递,先劳烦大家看一段 C++ 的代码(当然非常简单)。

void rFoo(int & ref) {
   printf("%p\n", &ref);// 0x7ffee5aef768
}
   
int main() {
   int a = 10;
   printf("%p\n", &a);// 0x7ffee7307768    int & b = a;
   printf("%p\n", &b);// 0x7ffee5aef768    rFoo(b);
   return 0; }

这里就是简单的在main中定义一个引用,然后传给函数 rFoo,那么来看看正统的引用传递是什么样的?

Go中没有引用传递

Go中函数调用只有值传递,但是类型引用有引用类型,他们是:slice、map、channel。来看看官方的说法:

There’s a lot of history on that topic. Early on, maps and channels were syntactically pointers and it was impossible to declare or use a non-pointer instance. Also, we struggled with how arrays should work. Eventually we decided that the strict separation of pointers and values made the language harder to use.  Changing these types to act as references to the associated, shared data structures resolved these issues. This change added some regrettable complexity to the language but had a large effect on usability: Go became a more productive, comfortable language when it was introduced.

大概意思是说:最开始用的是指针语法,由于种种原因改成了引用,但是这个引用与C++的引用是不同的,它是共享关联数据的结构。关于这个问题的深入讨论我会放到 slice 相关文章中进行讨论,现在回到今天讨论的主题。

那么Go的引用传递源起何处?我觉得让大家误解的是,map、slice、channel这类引用类型在传递到函数内部,可以在函数内部对它的值进行修改而引起的误会。

针对这种三种类型是 by value 传递,我们用 slice 来进行验证。

func main() {
    arr := [5]int{1, 3, 5, 6, 7}
    fmt.Printf("addr:%p\n", &arr)// addr:0xc42001a1e0
    s1 := arr[:]
    fmt.Printf("addr:%p\n", &s1)// addr:0xc42000a060

    changeSlice(s1)
}

func changeSlice(s []int) {    fmt.Printf("addr:%p\n", &s)// addr:0xc42000a080    fmt.Printf("addr:%p\n", &s[0])// addr:0xc42001a1e0
}

小结

  • Go 中函数传参仅有值传递一种方式;

  • slice、map、channel都是引用类型,但是跟c++的不同;

  • slice能够通过函数传参后,修改对应的数组值,是因为 slice 内部保存了引用数组的指针,并不是因为引用传递。

接下来的文章尝试解析下:
slice 为什么一定要用 make 进行初始话,它初始化做了哪些事情?它每次动态扩展容量的时候进行了什么操作?

以上是关于Golang中函数传参存在引用传递吗?的主要内容,如果未能解决你的问题,请参考以下文章

Golang 函数传参使用切片而不使用数组为什么?

Golang - 指针与引用

JS中函数参数值传递和引用传递

验证python中函数传参是引用传递

Java 函数传参

Javascript 之《函数传参到底是值传递还是引用传递》