
在 Go 中对共享切片进行并发写入(如 append)是典型的竞态条件场景;即使配合 sync.WaitGroup 等待所有 goroutine 完成,若缺乏同步机制(如 Mutex 或 RWMutex),仍会导致数据丢失、长度异常或 panic。
在 go 中对共享切片进行并发写入(如 `append`)是典型的竞态条件场景;即使配合 `sync.waitgroup` 等待所有 goroutine 完成,若缺乏同步机制(如 `mutex` 或 `rwmutex`),仍会导致数据丢失、长度异常或 panic。
切片([]T)在底层由三部分组成:指向底层数组的指针、长度(len)和容量(cap)。当多个 goroutine 同时调用 append 时,可能同时读取旧的 len 和 cap,并尝试写入同一块内存——这不仅会引发数据竞争(data race),还可能导致以下问题:
- 多个 goroutine 写入相同索引位置,覆盖彼此数据;
- 某次
append触发底层数组扩容后,新数组地址未被其他 goroutine 及时感知,导致后续写入仍在旧数组上,造成静默丢弃; - 运行时可能 panic(如
fatal error: concurrent map writes类似行为,虽切片本身不直接 panic,但竞态检测器go run -race会明确报错)。
在您提供的代码中,WithoutMutex 函数的问题根源正在于此:
func (nonmu *NonMuType) add(i int, wg *sync.WaitGroup) {
nonmu.list = append(nonmu.list, i) // ⚠️ 非原子操作:读 len/cap → 写数组 → 更新 slice header
wg.Done()
}该操作在并发下不是线程安全的。WaitGroup 仅确保“所有 goroutine 已退出”,不提供任何内存可见性或执行顺序保证,因此无法防止竞态。
而 WithMutex 版本通过 data.Lock()/Unlock() 序列化了 append 操作,确保每次只有一个 goroutine 修改 list,从而避免了数据竞争。其输出长度恒为 10 是预期行为(尽管元素顺序不确定,因 goroutine 调度无序)。
✅ 正确实践建议:
对共享可变状态(尤其是切片、map、结构体字段)的并发写入,必须加锁(
sync.Mutex或sync.RWMutex);-
若仅需收集结果且顺序无关,也可考虑「每个 goroutine 独立生成子切片,最后由主 goroutine 合并」的方式,避免锁竞争:
func WithoutMutexNoRace() []int { var results [][]int var wg sync.WaitGroup for i := 0; i < 10; i++ { wg.Add(1) go func(val int) { defer wg.Done() results = append(results, []int{val}) // 每个 goroutine 写自己的局部切片 }(i) } wg.Wait() // 主 goroutine 合并(单线程、安全) var merged []int for _, sub := range results { merged = append(merged, sub...) } return merged } 始终启用竞态检测:
go run -race main.go或go test -race,它能自动发现此类问题;注意:
sync.RWMutex的RLock/RUnlock仅对只读场景有意义;写操作(如append)必须使用Lock/Unlock。
总结:WaitGroup 是协作同步工具,不是并发安全的替代品。保障共享数据一致性,锁(或通道、原子操作等)不可省略。

















