Go 语言的参数传递

简介: 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 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)
  }


运行后我电脑上的结果,

1668503014982.jpg

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

1668503028521.jpg


传 *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"
}


运行结果,

1668503060658.jpg

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

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

1668503073528.jpg

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

如果传递的是 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/

相关文章
|
17天前
|
存储 Go 索引
go语言中数组和切片
go语言中数组和切片
27 7
|
17天前
|
Go 开发工具
百炼-千问模型通过openai接口构建assistant 等 go语言
由于阿里百炼平台通义千问大模型没有完善的go语言兼容openapi示例,并且官方答复assistant是不兼容openapi sdk的。 实际使用中发现是能够支持的,所以自己写了一个demo test示例,给大家做一个参考。
|
17天前
|
程序员 Go
go语言中结构体(Struct)
go语言中结构体(Struct)
92 71
|
16天前
|
存储 Go 索引
go语言中的数组(Array)
go语言中的数组(Array)
100 67
|
17天前
|
存储 Go
go语言中映射
go语言中映射
32 11
|
19天前
|
Go
go语言for遍历映射(map)
go语言for遍历映射(map)
29 12
|
18天前
|
Go 索引
go语言使用索引遍历
go语言使用索引遍历
26 9
|
18天前
|
Go 索引
go语言使用range关键字
go语言使用range关键字
24 7
|
18天前
|
Go 索引
go语言修改元素
go语言修改元素
25 6
|
9天前
|
Go 数据安全/隐私保护 UED
优化Go语言中的网络连接:设置代理超时参数
优化Go语言中的网络连接:设置代理超时参数