Go 语言的参数传递

前言

对于一门编程语言,在我们调用一个函数并且传递参数的时候,可能会下意识的去思考,到底是按值传递(by value) 还是按引用(by reference) 传递。

首先,在 Go 的 faq 中明确表示过所有东西都是按值传递的[1] ,并不存在引用传递。

As in all languages in the C family, everything in Go is passed by value. That is, a function always gets a copy of the thing being passed, as if there were an assignment statement assigning the value to the parameter.

但是我们在项目中经常会看到这样的代码,一会传 T,一会传 *T,传 *T 为啥就不是引用传递?

func changeString1(name string) {}
func changeString2(name *string) {}

传 T

我们先从传 T 的代码开始看,

package mainimport 'fmt'type user struct {  Name string}func main() {  var u user  u.Name = 'wuqq'  fmt.Printf('u的值:%+v,内存地址:%p\n', u, &u)  ChangeUser(u)  fmt.Printf('调用函数后的值:%+v,内存地址:%p\n', u, &u)}func ChangeUser(userInfo user) {  fmt.Printf('接收到u的值:%+v,内存地址:%p\n', userInfo, &userInfo)  userInfo.Name = 'curry'  fmt.Printf('修改后u的值:%+v,内存地址:%p\n', userInfo, &userInfo)  }

运行后我电脑上的结果,

可以看到,传递的参数 u (内存地址0xc000010200) 会创建一个副本(内存地址0xc000010220) 到函数 ChangeUser 中,在函数中对参数的修改不会影响到原始的值,因为此时,本质上是这样的。

传 *T

我们修改下上面的示例,修改成传 *T,

package main
import 'fmt'

type user struct {  Name string}
func main() {  u := &user{Name: 'wuqq'}  fmt.Printf('u的值:%+v,内存地址:%p,指针地址:%p\n', *u, u, &u)  ChangeUser(u)  fmt.Printf('调用函数后的值:%+v,内存地址:%p,指针地址:%p\n', *u, u, &u)}func ChangeUser(userInfo *user) {  fmt.Printf('接收到u的值:%+v,内存地址:%p,指针地址:%p\n', *userInfo, userInfo, &userInfo)  userInfo.Name = 'curry'}

运行结果,

首先,我们需要知道,任何存放在内存中的东西都有自己的地址。指针也一样,指针虽然指向的是别的数据,但是指针的本身也是需要内存空间进行存储的。

上面 u 指针它的内存地址是 0xc00000e028。当我们把 u 指针传递给函数时,会创建一个指针的副本(地址:0xc00000e038),只不过指针 0xc00000e028 和指针 0xc00000e038 都指向了同一个内存地址 0xc000010200。那么此时对 *user 的修改势必会影响到原始传入的值。因为,本质上他们指向的是同一个对象。

从上面可以看出,当一个变量被当作参数传递的时候,一定会创建这个变量的副本,即按值传递。

如果传递的是 T,创建的是参数的整个副本。

如果传递的是 *T,创建的是指针的副本。

虽然两个指针所存储的内存地址不一样,但是它们的值是相同的,指向了相同的内存地址。比如上面的 0xc00000e028 和 0xc00000e038 两个指针的内存地址不一样,但是都指向了 0xc000010200 这个内存。

因此就可以解释无论在 Go 中传递的是什么类型,本质上都是值传递。只是有时候拷贝的是非引用的类型,比如 int,string,struct...... ,这样无法在调用函数中修改原对象数据,

什么时候传 T,什么时候传 *T

更多的是看副本创建所需的成本和自己的需求。

  • 如果不想传递的变量被修改,那么就传 T。这样我们无需关心调用的函数对变量做了什么修改操作。

  • 大的结构体(struct) 或者数组。比如我们通过调用外部接口获取某些数据解析到对应的 struct 后,然后层层的把这个 struct 以 T 形式传入下游业务服务,那么会导致这个 T 频繁的创建副本,影响性能。这个时候可以考虑传递 *T,但是需要考虑是否会对业务的正确性造成破坏。

  • 对于函数作用域内的参数,如果定义成 T,Go 编译器会尽可能将对象分配到栈上,如果是 *T ,因为指针的存在,对象可能不能随着函数的结束而结束,进而导致对象被分配到堆上,对GC多少会产生影响。

  • ......

(0)

相关推荐