Go语言:为何Child切片创建位置影响goroutine中嵌套结构体修改结果?
Go中父结构体修改嵌套切片的并发问题解析
问题场景
我测试了两段Go代码,唯一区别就是childs切片的创建位置:
- 第一段代码里,
childs在goroutine外面创建,最后结果切片里所有Parent的Childs内容全一样 - 第二段代码把
childs的创建逻辑放到每个goroutine内部,结果就正常了,每个Parent的Childs内容都是独立的
第一段代码(结果异常)
package main import ( "fmt" "sync" ) const ( gr = 5 ) type Child struct { Grid int Child string } type Parent struct { Childs []Child Parent string } func (parent Parent) ChangeChild(gr int) Parent { for i, _ := range parent.Childs { parent.Childs[i].Grid = gr + i } return parent } func main() { var wg sync.WaitGroup var mu sync.Mutex var s []Parent var childs []Child for i := 0; i < 5; i++ { childs = append(childs, Child{Child: "child"}) } wg.Add(gr) for i := 0; i < gr; i++ { go func(i int) { defer mu.Unlock() defer wg.Done() parent := Parent{Childs: childs, Parent: " parent"} parent = parent.ChangeChild(i) mu.Lock() fmt.Println(parent) s = append(s, parent) }(i) } wg.Wait() fmt.Println(s) }
第二段代码(结果正常)
package main import ( "fmt" "sync" ) const ( gr = 5 ) type Child struct { Grid int Child string } type Parent struct { Childs []Child Parent string } func (parent Parent) ChangeChild(gr int) Parent { for i, _ := range parent.Childs { parent.Childs[i].Grid = gr + i } return parent } func main() { var wg sync.WaitGroup var mu sync.Mutex var s []Parent wg.Add(gr) for i := 0; i < gr; i++ { go func(i int) { defer mu.Unlock() defer wg.Done() var childs []Child for i := 0; i < 5; i++ { childs = append(childs, Child{Child: "child"}) } parent := Parent{Childs: childs, Parent: " parent"} parent = parent.ChangeChild(i) mu.Lock() fmt.Println(parent) s = append(s, parent) }(i) } wg.Wait() fmt.Println(s) }
问题根源
1. Go切片的本质是引用类型
Go里的切片不是值类型,它内部存的是指向底层数组的指针、长度和容量。当你把一个切片赋值给另一个变量时,只是拷贝了这三个字段,底层的数组还是同一个。
2. 第一段代码的共享冲突
第一段代码里,所有goroutine用的都是同一个childs切片的底层数组。哪怕ChangeChild是值接收者(会拷贝整个Parent结构体),拷贝后的Parent.Childs还是指向原来的底层数组。
多个goroutine并发修改这个数组时,谁最后执行,谁的修改就会覆盖之前所有的改动。最后所有Parent的Childs都指向这个被最后修改的数组,所以结果全一样。
3. 第二段代码的独立隔离
第二段代码里,每个goroutine都自己创建childs切片,意味着每个切片都有专属的底层数组。goroutine之间修改的是各自的数组,互相不干扰,自然结果就正常了。
踩坑提醒
别以为值接收者的方法就能完全隔离数据——对于切片、map这种引用类型来说,结构体拷贝只是复制了引用,底层数据还是共享的。要彻底避免这种问题,要么在每个goroutine里创建独立的引用类型实例,要么在方法内部重新拷贝一份数据。
内容的提问来源于stack exchange,提问作者faelis
相关产品推荐
相关产品推荐

