这篇文章想浅浅地讲解 Go 语言函数参数传递的值拷贝。
一句话观点
Go语言中所有传递都是值传递,严格来说并不存在引用传递的概念。传递指针只是传递指针的值,并不是引用传递,只不过通过指针可以间接修改变量的值,从而达到类似引用传递的效果。
值传递
值传递就是将参数的副本传递给函数,因此在函数内部修改参数的值,不会影响到原始变量的值。
func modifyValue(person Person) {
person.Name = "Alice"
}
func Test4(t *testing.T) {
person1 := Person{Name: "Bob"}
modifyValue(person1)
fmt.Println("值传递:", person1.Name) // 输出: 值传递: Bob
}
在这个例子中,modifyValue
函数接收 person
的副本,修改它的字段值并不会影响原来的 person
。
引用传递
要实现引用传递,可以通过传递指针来实现。
func modifyReference(person *Person) {
person.Name = "Alice"
}
func Test4(t *testing.T) {
person2 := &Person{Name: "Bob"}
modifyReference(person2)
fmt.Println("引用传递:", person2.Name) // 输出: 引用传递: Alice
}
在这个例子中,modifyReference
函数接收的是 person
指针类型的副本,修改副本指向的值,会影响到原来的。
值传递例子
下面再看一个值传递的例子。恰好最近在读《Go 语言精进之路》这本书,书里有一段讲解的方法的本质问题,案例是下面这段代码:
type field struct{ name string }
func (p *field) print() {
fmt.Println(p.name)
}
func main() {
data1 := []*field{{"one"}, {"two"}, {"three"}}
for _, v := range data1 {
go v.print()
}
data2 := []field{{"four"}, {"five"}, {"six"}}
for _, v := range data2 {
go v.print()
}
time.Sleep(3 * time.Second)
}
我对上面的代码进行简单改造,改为单测方法,打印输出内存地址,改造的代码如下:
func (p *field) print() {
fmt.Printf("%p %s \n", p, p.name)
}
func Test1(t *testing.T) {
data1 := []*field{{"one"}, {"two"}, {"three"}}
for _, v := range data1 {
go (*field).print(v)
}
data2 := []field{{"four"}, {"five"}, {"six"}}
for _, v := range data2 {
go (*field).print(&v)
}
time.Sleep(3 * time.Second)
}
运行单测,看下结果:
0xc000026590 six
0xc000026590 six
0xc000026590 six
0xc000026540 two
0xc000026530 one
0xc000026550 three
如果你奇怪为什么第二段代码输出的全是 six
,那么可以接着看下面的原因分析。
先要知道的是, for range
循环中会重复使用同一个变量 v
。
在第一段,data1
是一个指针数组([]*field
),v
每次迭代时是指向数组中不同元素的指针。由于 v
是指针,每个元素的地址自然是不同的,因此打印出的地址不同,每个元素都可以打印出来。
在第二段,data2
是一个结构体数组([]field
),而 v
是数组元素的副本。当你取 &v
时,每次获取的都是这个循环中的同一个变量的地址,所以 &v
的地址是相同的。但每次迭代时,这个地址指向的值会被更新为 data2
中当前元素的副本,在最后执行的时候,&v
执行的值就是 six
。
如果想让第二段输出所有元素,可以每次迭代拷贝一个 v
副本,复制给变量 s
,如下:
for _, v := range data2 {
s := v
go (*field).print(&s)
}
执行单测,输出结果如下:
0xc0000265d0 six
0xc000026590 four
0xc0000265b0 five
0xc000026540 two
0xc000026550 three
0xc000026530 one
在这个例子中,每次迭代都会创建新的变量 s
,内存地址是不同的,所以可以全部输出。
Over!
微软开源基于 Rust 的 OpenHCL 字节跳动商业化团队模型训练被“投毒”,内部人士称未影响豆包大模型 华为正式发布原生鸿蒙系统 OpenJDK 新提案:将 JDK 大小减少约 25% Node.js 23 正式发布,不再支持 32 位 Windows 系统 Linux 大规模移除疑似俄开发者,开源药丸? QUIC 在高速网络下不够快 RustDesk 远程桌面 Web 客户端 V2 预览 前端开发框架 Svelte 5 发布,历史上最重要的版本 开源日报 | 北大实习生攻击字节AI训练集群;Bitwarden进一步脱离开源;新一代MoE架构;给手机装Linux;英伟达真正的护城河是什么?