Go语言不保证并发安全,需显式加锁或使用并发安全原语;sync.Mutex用于保护非原子共享数据,sync.RWMutex适合读多写少,sync.Map适用于高并发读且键值生命周期长的场景;channel、atomic、Once、singleflight等天生安全。

Go 语言本身不保证并发安全,必须显式加锁或选用并发安全原语——这是最常被新手忽略的前提。 没有自动同步的变量、结构体字段、切片、map(除 sync.Map)、函数局部变量以外的任何共享状态,只要被多个 goroutine 同时读写,就存在竞态风险。
什么时候必须用 sync.Mutex?
当你需要保护「非原子操作」的共享数据时,sync.Mutex 是最直接、最可控的选择。比如对一个整数做 counter++,它实际是「读→改→写」三步,中间可能被其他 goroutine 插入,导致丢失更新。
- 适用场景:自定义结构体字段读写、手动管理的切片/队列/栈、配置热更新、资源计数器等
- 关键原则:所有访问该共享数据的代码路径,都必须经过同一把
mu.Lock()/mu.Unlock()包裹 - 常见错误:只在写操作加锁,读操作漏锁;或在 defer 前 panic 导致 unlock 没执行(应始终用
defer mu.Unlock()) - 性能提示:锁粒度越细越好。不要把无关逻辑(如日志、HTTP 调用)塞进临界区
sync.RWMutex 和 sync.Map 怎么选?
读多写少时,sync.RWMutex 比普通 Mutex 更高效,因为允许多个 goroutine 并发读;而 sync.Map 是标准库专为高并发读设计的 map 实现,但写操作开销更大,且不支持遍历中途修改。
-
sync.RWMutex:适合你已有map[string]T或其他结构,且能自己控制读写逻辑(例如用RLock()/Unlock()读,Lock()/Unlock()写) -
sync.Map:适合键值对生命周期长、读远大于写、且不需要遍历时 delete 的场景;但它不支持类型安全(value 是interface{}),也没有LoadOrStore以外的复合操作 - 别踩坑:不要用
sync.Map存放指针并期望它保护指针指向的结构体字段——它只保证 map 自身操作安全,不递归保护 value 内容
哪些东西天生并发安全,不用加锁?
Go 运行时已为你处理好同步的机制,可放心并发使用:
立即学习“go语言免费学习笔记(深入)”;
-
channel:无论有无缓冲,send和receive操作都是原子且线程安全的。生产者-消费者模型首选,无需额外锁 -
sync/atomic:对int32、int64、uint32、uintptr、unsafe.Pointer等提供原子增减、交换、比较并交换(CAS)。适用于标志位、计数器、轻量状态切换 -
sync.Once:确保某个函数只执行一次,常用于单例初始化。内部用互斥锁+原子标志实现,调用方完全无感 -
singleflight.Group:合并重复请求,避免缓存击穿或 DB 重复查询。它内部用sync.Mutex+map+sync.WaitGroup封装,对外暴露简洁的Do(key, fn)
最容易被忽视的并发陷阱
不是锁没加,而是加得不对或没意识到共享发生在哪:
- 闭包中捕获循环变量:
for i := range items { go func() { use(i) }() }→ 所有 goroutine 共享同一个i变量,最终看到的可能是最后一个值。应传参:go func(v int) { use(v) }(i) - 切片底层数组共享:两个 goroutine 分别拿到同一底层数组的切片,再各自
append,可能互相覆盖。需深拷贝或用sync.Pool管理 - 指针方法并发调用:如果方法修改了接收者字段,而该指针被多个 goroutine 共享,又没加锁或原子操作,就是典型数据竞争
- 误信「只读就安全」:若读操作依赖多个字段的一致性(比如先读
a再读b),而另一 goroutine 正在更新二者,仍需锁保证整体原子性
并发安全不是靠猜,也不是加把锁就万事大吉。真正要盯住的是「共享状态的边界」和「操作的原子性范围」——这两点没理清,再多的锁也防不住 bug。










