Go 结构体方法中,有一个很重要的点就是值传递和引用传递,我们通过一个例子来看下什么是值传递、什么是引用传递,二者有什么区别。
我们声明 person
结构体,里边有一个 name
字段,用两种方式实现 SetName
方法,分别是值传递和引用传递。
1 | type person struct { |
如上,SetName1
就是值传递,SetName2
为引用传递。
在 main
方法中,我们分别调用两个 SetName
方法对 name
进行赋值,并打印每次赋值后的 name
值。
1 | func main() { |
执行这个程序得到如下结果:
1 |
|
可以看到 张三
并没有打印出来,而 李四
打印了出来。
在调用 SetName1
时,实际上是复制了一个新的 person
,方法内操作的也是那个新 person
把复制 person
的 name
改为了张三,而我们在 main 方法中打印的确是原始 person
的 name
字段。因为我们在初始化 person
时并没有指定 name
的值,所以第一次打印出来的是个空串。
对程序稍作调整,先调用 SetName2
再调用 SetName1
:
1 | func main() { |
这时输出的结果为:
1 | 李四 |
第一次调用后,p
结构体指针中 name
的值已经被改为了 李四
,接下来我们调用 SetName1
时,因为是复制了一个新的 person
,并没有影响之前的 person
,所以打印结果还是 李四
。
**我们日常开发中,编写结构体方法时大部分情况都是用引用传递。
值传递的问题是,如果我们结构体的成员数量非常多时,每次调用方法都会进行一次拷贝,会有额外的内存开销。
验证一下值传递有没有分配新的内存:
1 | type person struct{ |
运行结果:
1 | Origin: 0xc000010200 |
可以看到,原始的 person
地址为 0xc000010200,在通过值传递时位置发生了改变,变为了0xc000010210,这也就意味着系统为这个新的 person
分配了新的内存地址,而用引用传递的方式地址是不会变的。
引用传递容易犯的错误
我们假设要实现一个发送邮件的功能,定义一个 email
结构体,里边有两个成员 from
和 to
,实现两个方法用来更新这两个成员变量。
1 | type email struct { |
再实现一个发送邮件的方法,这里简单将 from
和 to
打印出来即可:
1 | func (e *email) Send() { |
在 main
方法中,我们写一个循环,实现发送 10 次邮件,0 发送给 1,1 发送个 2,一次以此类推:
1 | func main() { |
输出如下:
1 | from: 0, to: 1 |
这时候,如果我们改成并发发送这些邮件,同时发给10个人,很容易就会把上边的代码改写如下:
1 | func main() { |
再次运行,结果如下:
1 | from: 2, to: 3 |
没有按照递增的顺序发送是在我们意料之中的,但是我们可以看到其中有一行输出为:from: 9, to: 1
,这个并不是我们想要的结果。
出现这个问题的原因是在每个 go routine
中都是对原始 email
进行的修改,再并发操作的过程中,from
和 to
有可能被其他的 go routine
改掉,这是个非常严重的 bug。
两种修改方法
每一次都初始化一个新的 email 结构体:
1 | func main() { |
改用值传递,并返回修改后的结构体
1 | package main |
读者可以自己想一想,为什么这种写法可以解决并发赋值出现的问题。