不安全。append可能触发底层数组扩容导致指针替换,其他goroutine读取时可能访问旧指针,引发脏读、越界panic或数据损坏;必须用锁保护所有读写操作,或改用channel由单个goroutine串行聚合。

直接用 mutex 包裹 append 就安全了吗?
不安全。即使你每次 append 前都 mu.Lock()、结束后 mu.Unlock(),仍可能出问题——因为 append 可能触发底层数组扩容,导致指针(slice.header.data)被替换。其他 goroutine 若正在读这个切片(比如调用 len() 或访问 s[i]),可能拿到旧指针,造成脏读、越界 panic 或静默数据损坏。
实操建议:
- 锁必须覆盖所有访问路径:包括
len()、cap()、索引读写、append、copy,不能只锁写不锁读 - 不要把切片字段直接暴露出去(如提供
GetSlice() []T方法),否则锁形同虚设 - 若读多写少,优先用
sync.RWMutex,但注意RLock()仍需包裹全部读操作
用 channel 收集结果比共享切片更可靠
当多个 goroutine 各自生成数据(如解析日志、处理图像子块、调用 API),最终要聚合到一个切片时,别让它们并发写同一底层数组。正确做法是:每个 goroutine 把结果发到同一个 chan T,由**单个消费者 goroutine** 串行接收并 append 到本地切片。
这样既避免竞争,又天然满足 happens-before(channel send/receive 构成同步边界)。示例关键逻辑:
立即学习“go语言免费学习笔记(深入)”;
ch := make(chan *Song, 100)
go func() {
for song := range ch {
playlist.songs = append(playlist.songs, song) // 只有这一个 goroutine 写
}
}()
// 其他 goroutine 只做:ch <- newSong
常见错误:
- 开多个 goroutine 同时从同一
chan接收并各自append→ 竞态重现 - 通道未关闭,消费者 goroutine 永远阻塞 → 忘记在所有生产者退出后
close(ch)
预分配 + 固定索引写入:零锁高性能方案
如果结果切片长度已知(例如并发处理一个输入切片的每个元素),且每个 goroutine 负责唯一位置,就完全不用锁或 channel。
做法是主协程先 make([]T, len(input)),然后每个 goroutine 通过传入的索引直接写 out[i] = result。只要索引不重叠,底层数组写入互不干扰。
必须注意:
- 循环变量必须显式传参,避免闭包捕获(
go func(i int) { out[i] = ... }(i),不能写go func() { out[i] = ... }()) - 务必提前
wg.Add(1),且在 goroutine 内defer wg.Done() - 若计算可能 panic,需在 goroutine 内加
recover,否则WaitGroup无法完成
分片(sharding)适合高频写+大容量场景
当写入频次极高(如每秒数万次)、且允许轻微顺序打乱时,可将切片逻辑拆成多个独立分片,每个分片配独立 sync.RWMutex。写入时按哈希(如 val % numShards)路由到对应分片。
优点是锁粒度细,吞吐量高;缺点是遍历时需合并多个分片,且无法保证全局插入顺序。
典型结构:
type ShardSlice struct {
shards []struct {
data []int
mu sync.RWMutex
}
numShards int
}
func (s *ShardSlice) Append(val int) {
idx := val & (s.numShards - 1)
s.shards[idx].mu.Lock()
s.shards[idx].data = append(s.shards[idx].data, val)
s.shards[idx].mu.Unlock()
}
容易忽略的点:
-
numShards必须是 2 的幂,才能使&运算等效于取模 - 分片数不是越多越好——过多会增加内存碎片和锁管理开销,一般从 4 或 8 开始压测调整
- 读操作若需全量,必须对每个分片加
RLock(),不能只读不锁
最常被低估的是内存可见性:即使你用 sync.Mutex 正确保护了写,若读端不加锁或不走 channel,仍可能看到过期值——Go 不保证非同步路径下的缓存一致性。


















