You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.30 15:15:11