
在 go 中对共享切片进行并发写入(如多次调用 append)时,若未使用互斥锁保护,将引发数据竞态,导致元素丢失、长度异常或 panic;sync.waitgroup 仅用于同步 goroutine 完成状态,无法保证内存访问安全。
在 go 中对共享切片进行并发写入(如多次调用 append)时,若未使用互斥锁保护,将引发数据竞态,导致元素丢失、长度异常或 panic;sync.waitgroup 仅用于同步 goroutine 完成状态,无法保证内存访问安全。
为什么 WithoutMutex 会丢数据?
切片([]int)本身不是并发安全的数据结构。其底层由三部分组成:指向底层数组的指针、长度(len)和容量(cap)。当多个 goroutine 同时执行 append 时,可能同时读取/修改同一份 len 和 cap,甚至触发底层数组扩容——而扩容涉及分配新数组、复制旧数据、更新指针,这些操作非原子。若两个 goroutine 并发执行扩容,其中一个的复制结果可能被另一个覆盖,最终导致部分元素“消失”。
例如,在你的 WithoutMutex 中:
nonmu.list = append(nonmu.list, i) // ❌ 非原子:读 len → 写新 len → 可能扩容 → 写指针
即使 WaitGroup 正确等待所有 goroutine 结束,它不提供任何内存可见性或互斥保障——它只确保“执行完毕”,而非“执行正确”。
为什么 WithMutex 能修复问题?
MuType.add() 方法通过 data.Lock() / defer data.Unlock() 确保每次仅有一个 goroutine 进入临界区执行 append,从而:
- 避免了对
len/cap/底层数组指针的并发读写; - 保证了
append操作的原子性和内存可见性(写入对其他 goroutine 立即可见); - 最终稳定输出 10 个元素(尽管顺序随机,因 goroutine 调度不可控)。
✅ 正确示例(精简版):
func (m *MuType) Add(i int) {
m.Lock()
defer m.Unlock()
m.list = append(m.list, i)
}
更优实践建议
-
避免暴露可变共享状态:不要让
list字段直接可读写。提供受控方法(如Add,GetAll),并在内部加锁。 -
考虑
sync.Map或通道替代方案:若场景是键值聚合,sync.Map更合适;若需顺序收集,可用带缓冲 channel + 单个 collector goroutine:ch := make(chan int, 10) go func() { for v := range ch { data.list = append(data.list, v) } }() // 各 goroutine 发送:ch -
启用竞态检测器验证:运行
go run -race main.go,它会明确报告slice append的数据竞态位置。
总结
- ✅
sync.Mutex/sync.RWMutex是保护共享可变状态(如切片、map、结构体字段)的必需手段; - ❌
sync.WaitGroup是协作同步工具,绝不等价于线程安全; - ? 所有并发写入共享内存的操作,必须通过锁、原子操作(
atomic)、通道或不可变数据结构来保证安全性; - ?️ 在生产代码中,应默认为共享可变状态添加同步机制,并始终用
-race进行测试。
忘记加锁的并发切片操作,不是“偶尔出错”,而是“必然不可靠”——这是 Go 内存模型的基本约束,而非平台或版本差异。










