先说结论
golang中,值类型在作为方法参数和方法的接受者的时候,都需要进行值的拷贝,所以,使用值类型的时候要多加注意。 对于方法的接受者,如果方法需要修改接受者的某个变量值,那么就应该把接受者设计成pointer receiver,否则对于receiver变量的修改将无效。
问题由来
今天群里有人发了下面的代码:
type data struct {
sync.Mutex
}
func (d data) test(s string) {
d.Lock()
defer func() {
d.Unlock()
println("success")
}()
for i := 0; i < 5; i++ {
fmt.Println(s, i)
time.Sleep(time.Second)
}
}
func main() {
var wg sync.WaitGroup
wg.Add(2)
var d data
go func() {
defer wg.Done()
d.test("read")
}()
go func() {
defer wg.Done()
d.test("write")
}()
wg.Wait()
}
这段代码的运行结果如下:
write 0
read 0
read 1
write 1
read 2
write 2
read 3
write 3
write 4
read 4
success
success
对此他的疑问是:为什么会是这样呢?read和write为什么是交替执行?程序里面加了锁,锁为什么没生效呢? 因为如果锁生效,结果应该是先输出所有的write或者read,然后再输出另外一个。
问题原因
先看代码,上面代码中方法test
的接受者为data,注意,这个data值类型(value receiver)。 在golang中,对于值类型在进行参数传递的时候传递的是值的拷贝。 在上面main方法中,两个go关键字开启了两个routine,这里面的两个d实际上不是同一个。所以在执行的过程中,锁也不是同一个,所以就不会出现锁失效的情况。
原因验证
我们可以通过输出一下对象的地址来看一下。 把test方法改一下:
func (d data) test(s string) {
d.Lock()
defer func() {
d.Unlock()
println("success")
}()
for i := 0; i < 5; i++ {
fmt.Printf("%s, %d, object addr: %p \n", s, i, &d)
time.Sleep(time.Second)
}
}
我们通过fmt.Printf("%p")
来输出一下d的地址。其他代码不变。运行结果如下:
write, 0, object addr: 0xc82000ae78
read, 0, object addr: 0xc8200ce000
read, 1, object addr: 0xc8200ce000
write, 1, object addr: 0xc82000ae78
read, 2, object addr: 0xc8200ce000
write, 2, object addr: 0xc82000ae78
write, 3, object addr: 0xc82000ae78
read, 3, object addr: 0xc8200ce000
read, 4, object addr: 0xc8200ce000
write, 4, object addr: 0xc82000ae78
success
success
可见,输出的d的地址并不相同。也就是说,实际执行的时候,是有两个data对象的,所以锁也不同,达不到公用锁的目的,所以输出结果就是乱序的。
是不是与匿名组合有关?
data的定义:
type data struct {
sync.Mutex
}
结构体中的sync.Mutex
没有命名。这种方式被称为匿名组合。匿名组合可以使一个结构体具有被匿名组合的结构的方法。类似于java中的继承。 那么是不是匿名组合导致的呢?答案是否定的,其实前面已经提到了,根源在于test方法的接受者是一个值类型而不是引用类型,从而导致两个goroutine中test方法的执行实际上是基于两个不同的data对象,所以它们的锁也不同。无论是否匿名组合都会有这个问题。下面的代码去掉了匿名组合:
type data struct {
lock sync.Mutex
}
func (d data) test(s string) {
d.lock.Lock()
defer func() {
d.lock.Unlock()
println("success")
}()
for i := 0; i < 5; i++ {
fmt.Printf("%s, %d, object addr: %p, lock address: %p \n", s, i, &d, &(d.lock))
time.Sleep(time.Second)
}
}
运行结果如下:
write, 0, object addr: 0xc82000ae78, lock address: 0xc82000ae78
read, 0, object addr: 0xc8200ce000, lock address: 0xc8200ce000
read, 1, object addr: 0xc8200ce000, lock address: 0xc8200ce000
write, 1, object addr: 0xc82000ae78, lock address: 0xc82000ae78
read, 2, object addr: 0xc8200ce000, lock address: 0xc8200ce000
write, 2, object addr: 0xc82000ae78, lock address: 0xc82000ae78
write, 3, object addr: 0xc82000ae78, lock address: 0xc82000ae78
read, 3, object addr: 0xc8200ce000, lock address: 0xc8200ce000
read, 4, object addr: 0xc8200ce000, lock address: 0xc8200ce000
write, 4, object addr: 0xc82000ae78, lock address: 0xc82000ae78
success
success
另一个解决办法
对于这个问题,除了把test方法的receiver改成pointer receiver之外,还有没有其他办法呢?答案是肯定的,只需要把匿名组合中的锁改成data结构体中显示变量,然后把锁由值类型改成引用类型即可。代码如下:
type data struct {
lock *sync.Mutex
}
main方法中把var d data
改一下:
//var d data
var d data = data{lock:&sync.Mutex{}}
完整代码如下:
type data struct {
lock *sync.Mutex
}
func (d data) test(s string) {
d.lock.Lock()
defer func() {
d.lock.Unlock()
println("success")
}()
for i := 0; i < 5; i++ {
fmt.Printf("%s, %d, object addr: %p, lock address: %p \n", s, i, &d, (d.lock))
time.Sleep(time.Second)
}
}
func main() {
var wg sync.WaitGroup
wg.Add(2)
//var d data
var d data = data{lock:&sync.Mutex{}}
go func() {
defer wg.Done()
d.test("read")
}()
go func() {
defer wg.Done()
d.test("write")
}()
wg.Wait()
}
运行结果:
write, 0, object addr: 0xc82002e028, lock address: 0xc82000ae78
write, 1, object addr: 0xc82002e028, lock address: 0xc82000ae78
write, 2, object addr: 0xc82002e028, lock address: 0xc82000ae78
write, 3, object addr: 0xc82002e028, lock address: 0xc82000ae78
write, 4, object addr: 0xc82002e028, lock address: 0xc82000ae78
success
read, 0, object addr: 0xc8200d8000, lock address: 0xc82000ae78
read, 1, object addr: 0xc8200d8000, lock address: 0xc82000ae78
read, 2, object addr: 0xc8200d8000, lock address: 0xc82000ae78
read, 3, object addr: 0xc8200d8000, lock address: 0xc82000ae78
read, 4, object addr: 0xc8200d8000, lock address: 0xc82000ae78
success
由上面运行结果可以看出,data对象d在实际运行过程中依然是两个对象,但是它们的锁却是相同的,这样也能达到同步的目的。 为什么呢? 因为虽然值类型在方法传递的时候会进行一次拷贝,但是对于指针类型的字段来说,拷贝的是指针的地址,所以两个地址实际上是一样的,都指向同一把锁。
data的值是何时拷贝的呢?
golang中,方法传递是值传递,在作为方法参数的时候,如果是值类型,将会对数据进行一次拷贝,这样在方法中对于参数的修改不会影响原来的值。 但是在第一个版本代码的main方法中,并没有对data类型变量d进行参数传递,那么d是如何被拷贝的呢?我们输出一下d的地址:
type data struct {
sync.Mutex
}
func (d data) test(s string) {
d.Lock()
defer func() {
d.Unlock()
println("success")
}()
for i := 0; i < 5; i++ {
fmt.Printf("%s, %d, object addr: %p \n", s, i, &d)
time.Sleep(time.Second)
}
}
func main() {
var wg sync.WaitGroup
wg.Add(2)
var d data
fmt.Printf("initial address of d: %p \n", &d)
go func() {
defer wg.Done()
fmt.Printf("1 address of d: %p \n", &d)
d.test("read")
}()
go func() {
defer wg.Done()
fmt.Printf("2 address of d: %p \n", &d)
d.test("write")
}()
wg.Wait()
}
结果如下:
initial address of d: 0xc820072da8
2 address of d: 0xc820072da8
write, 0, object addr: 0xc820072e60
1 address of d: 0xc820072da8
read, 0, object addr: 0xc82000a0e0
write, 1, object addr: 0xc820072e60
read, 1, object addr: 0xc82000a0e0
write, 2, object addr: 0xc820072e60
read, 2, object addr: 0xc82000a0e0
write, 3, object addr: 0xc820072e60
read, 3, object addr: 0xc82000a0e0
read, 4, object addr: 0xc82000a0e0
write, 4, object addr: 0xc820072e60
success
success
通过结果我们发现,在main方法中,两个go关键字开始的goroutine中,输出的d的地址都是相同的,都为“0xc820072da8”,而真正执行test方法之后,输出的地址却成了“0xc82000a0e0”和“0xc820072e60”。显然,d是进行了拷贝的。我们保持其他代码不变,把goroutine去掉,用单线程试一下:
func main() {
var d data
fmt.Printf("initial address of d: %p \n", &d)
d.test("read")
d.test("write")
}
输出结果:
initial address of d: 0xc82000ae78
read, 0, object addr: 0xc82000af20
read, 1, object addr: 0xc82000af20
read, 2, object addr: 0xc82000af20
read, 3, object addr: 0xc82000af20
read, 4, object addr: 0xc82000af20
success
write, 0, object addr: 0xc8200cc038
write, 1, object addr: 0xc8200cc038
write, 2, object addr: 0xc8200cc038
write, 3, object addr: 0xc8200cc038
write, 4, object addr: 0xc8200cc038
success
可以看到,同样的,一共出现了三个不同的地址,也就是说,test方法调用了两次,每次都是在不同的data对象上执行的。
那么我们可以得出结论:test方法在执行的时候,它的receiver如果是值类型,那么每次方法执行也需要进行一次拷贝。 总结一下:对于值类型来说,无论是作为参数传递给其他方法还是作为方法的receiver,都要进行值的拷贝。(因为值拷贝的目的就是为了避免对于值的修改会影响原来的值,所以对于方法的接受者或者方法参数来讲,处理逻辑都是一样的。)
总结:
golang中,值类型在作为方法参数和方法的接受者的时候,都需要进行值的拷贝,所以,使用值类型的时候要多加注意。 对于方法的接受者,如果方法需要修改接受者的某个变量值,那么就应该把接受者设计成pointer receiver,否则对于receiver变量的修改将无效。 比如:
type Person struct {
name string
}
func (p Person) change(newName string) {
p.name = newName
}
func main() {
var p Person = Person{"jason"}
p.change("john")
fmt.Println(p.name)
}
输出结果为jason
。
另:关于value receiver 和 pointer receiver可以参考golang官方的Effective Go中的说明:https://golang.org/doc/effective_go.html#pointers_vs_values