前言
对于一门编程语言,在我们调用一个函数并且传递参数的时候,可能会下意识的去思考,到底是按值传递(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 main import "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多少会产生影响。
- ......
相关阅读
[1]https://golang.org/doc/faq#pass_by_value[2]https://www.flysnow.org/2018/02/24/golang-function-parameters-passed-by-value.html[3]https://colobu.com/2017/01/05/-T-or-T-it-s-a-question/