Go语言中并发写同一slice不安全,因append和索引赋值非原子操作,扩容或共享底层数组会导致数据竞争;应使用Mutex、channel或原子索引等同步机制保障安全。

Go语言中直接并发写同一个slice不安全
多个goroutine同时对同一个slice变量执行append或通过索引赋值(如s[i] = x),会导致数据竞争(data race)。Go runtime在开启-race检测时会明确报出类似Write at 0x... by goroutine N和Previous write at 0x... by goroutine M的错误。
为什么slice并发写会出问题
slice底层是三元结构:ptr(指向底层数组)、len、cap。并发调用append可能触发扩容,这时会分配新数组、复制数据、更新ptr和len——这些操作不是原子的。即使没扩容,多个goroutine同时写同一索引位置,也会覆盖彼此结果。
-
append不是原子操作:读len→检查cap→可能分配→复制→写新len - 底层数组共享:不同goroutine看到的是同一块内存地址,无同步机制就等于裸写
- 编译器不会自动加锁:Go不提供“线程安全slice”这种内置类型
常见安全替代方案
没有银弹,选哪种取决于使用场景:
- 用
sync.Mutex或sync.RWMutex保护整个slice变量(适合读多写少、写操作不频繁) - 改用
chan做生产者-消费者模式:每个goroutine发数据到channel,由单个goroutine收集到slice(适合写入逻辑可解耦) - 预分配足够大的slice + 使用
sync/atomic管理索引:例如var idx int64,用atomic.AddInt64(&idx, 1)获取唯一下标后写入(要求长度已知且不扩容) - 用
sync.Map替代?不行——它只支持interface{}键值,不适合顺序集合场景
示例(原子索引写入):
data := make([]int, 1000)
var nextIdx int64
for i := 0; i < 100; i++ {
go func() {
pos := int(atomic.AddInt64(&nextIdx, 1)) - 1
if pos < len(data) {
data[pos] = pos * 2
}
}()
}
容易被忽略的隐式并发写场景
有些写法看似安全,实则仍有风险:
立即学习“go语言免费学习笔记(深入)”;
- 把slice作为参数传给多个goroutine,函数内部调用
append——传的是header副本,但ptr仍指向同一底层数组 - 用
for range启动goroutine,循环变量复用导致所有goroutine看到同一个v值(虽不直接写slice,但常伴随误写slice[v] = ...) - struct里嵌套slice字段,多个goroutine修改该struct实例——只要没同步,字段级写仍是竞态
真正安全的边界只有一条:**同一底层数组地址 + 多个写操作 → 必须同步**。别依赖“好像没出错”来判断,一定要跑go run -race验证。










