参数传递

在Go中函数参数传递都是值传递,都会进行拷贝,比如对于函数func(a *A, b B)(aa, bb),此时a是aa的副本,b是bb的副本。

Go中没有引用传递

传递指针还是传递值(在什么时候传递指针,什么时候传递值)

在如下情况需要传递指针:

  1. 需要在函数(或方法)中修改变量的值(如对象的属性)

  2. 需要传递的对象是一个大的结构体(struct)或者是一个大的数组,原因是给该对象的指针创建副本比直接给该对象创建副本的代价要小。

如果是在函数作用域内定义变量则建议将对象定义为值类型,因为Go编译器尽量将对象分配到栈上,如果定义为指针类型则可能会被分配到堆上(会对垃圾回收产生影响)

引用传递(传引用,C++支持,Go不支持)

go并不支持引用传递。引用传递是类似于func(&a A)(aa),对于a、aa的内存地址是一样,那么该函数是引用传递。但实际上在go中传递的指针之间内存地址并不一样,由于拷贝它们有着不同的地址

值传递

go的参数传递过程中都会对原始数据进行拷贝(创建副本,在函数中参与运算的是其副本),我们称这种传递方式为值传递(只是说这些副本有些是某个变量的副本,有些是某个变量的指针的副本)

副本的创建

给值类型创建副本

我们在函数中修改值对象的副本并不会对原始对象产生影响(原始对象的值不变),副本对象的内存地址和原始对象的内存地址不同。

给指针类型创建副本

我们在函数中通过值对象的指针修改属性的值,原始对象的该属性值也会变化,但是指针副本对象的内存地址和原始对象的内存地址不同。通过指针修改的属性值能生效的原因是,原始指针对象、副本指针对象指向同一块内存地址

什么时候创建副本

赋值的时候就会创建副本,各副本内存地址都不同, 如下所示:

对于array、slice以及map在初始化以及按索引设置值时会创建副本

for-range循环也是将元素的副本赋值给循环变量,所以变量得到的是集合元素的副本

也许你已经注意到了,循环变量的地址是一样的,因为在go的for range中循环变量是重用的

往channel中send元素也会创建对象副本

总结

  1. 在Go中函数参数传递方式是值传递

  2. 值传递时会进行拷贝(创建副本,副本有自己的内存地址且各不相同)

  3. 将变量作为参数传递给函数和方法会发生副本的创建。

  4. 对于返回值,将返回值赋值给其它变量或者传递给其它的函数和方法,就会创建副本

  5. 因为方法(method)最终会产生一个receiver作为第一个参数的函数(参看规范,另外我们在编写单元测试时,使用gomonkey去mock某个对象的方法时,第一个参数需要指定其receiver) 当receiver为T类型时,会发生创建副本,调用副本上的方法。 当receiver为*T类型时,只是会创建对象的指针,不创建对象的副本,方法内对receiver的改动会影响原始值。

  6. 指针类型创建副本的开销很小

  7. bool、数值类型一般不必考虑指针类型,原因在于这些对象很小,创建副本的开销可以忽略

  8. array(数组)是值类型,在参数传递或赋值的时候会创建副本,对于大的数组的参数传递或赋值的时候如果对值进行拷贝开销会很大,因此可以设计为[...]*T类型(某类型指针数组)

  9. map、slice和channel是引用类型,他们本身就是引用了其他类型来存储数据。一般来说不需要定义为指针类型

  10. 字符串在空省时默认值时"",如果你需要默认值为nil则需要定位为*string类型。在序列化对象时""表示字段存在且值为空字符串,nil表示字段不存在。string与[]byte直接进行强制转换时会发生数据的复制,用下面方式进行转换性能更好:

  1. 函数也是一个指针类型,对函数对象的赋值只是又创建了一个对次函数对象的指针。

解惑

  1. 传递指针时我们可以通过它修改原来的值,怎么会是一个拷贝呢?

    在给指针类型创建副本的实验中,我们通过输出的内存地址可以确认参数传递过来的指针是原始指针的一个副本。至于为什么可以通过指针副本修改原来的值,原因是指针副本和原始指针指向的是同一个内存地址。

    指针传递解释

  2. 对于map、slice、channel在函数中修改为什么原始对象的值也会改变?

    首先map、slice、channel都是引用类型,我们传递给函数的副本和原始对象所数据指向的内存地址(指针)是同一个

引用类型:map、slice、channel

源码见:src/runtime

map

对于map来说,当我们使用make对其初始化的时候实际上返回的是*hmap, 就是说map === *hmap

chan

类似map,当我们使用make对chan进行初始化的时候实际上返回的是*hchan, 就是说chan === *hchan

slice

我们先看一下slice的定义,他其实是一个结构体,数据存储在array中(用一个字段存储其内存地址)

和之前一样,我们看看使用make初始化时得到的是什么,emm...看起来不像之前的map、chan那样明显,难道有什么不可描述的“交易”

其实在src/reflect/value.go里面有一个这样的结构用于表示slice

所以在函数中我们可以通过下标修改slice的副本时,原始对象该下标的值也会改变。

注意:指针赋值操作

参考

[]T 还是 []*T, 这是一个问题

Go语言参数传递是传值还是传引用

深入解析Go中Slice底层实现

最后更新于

这有帮助吗?