先说结论
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,object addr: 0xc82000ae78 read,object addr: 0xc8200ce000 read,1,object addr: 0xc8200ce000 write,2,object addr: 0xc82000ae78 write,3,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,object addr: %p,lock address: %p \n",&d,&(d.lock)) time.Sleep(time.Second) } }
运行结果如下:
write,object addr: 0xc82000ae78,lock address: 0xc82000ae78 read,object addr: 0xc8200ce000,lock address: 0xc8200ce000 read,lock address: 0xc8200ce000 write,lock address: 0xc82000ae78 write,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.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,object addr: 0xc82002e028,lock address: 0xc82000ae78 success read,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) 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,object addr: 0xc820072e60 1 address of d: 0xc820072da8 read,object addr: 0xc82000a0e0 write,object addr: 0xc820072e60 read,object addr: 0xc82000a0e0 read,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,object addr: 0xc82000af20 read,object addr: 0xc82000af20 success write,object addr: 0xc8200cc038 write,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
原文链接:https://www.f2er.com/go/189159.html