当多个goroutine同时读写非原子变量时必须加锁,否则触发data race;常见场景包括全局计数器、并发读写map、结构体字段竞争等。

Go里什么时候必须加锁
当多个 goroutine 同时读写同一个变量(尤其是写),且该变量不是原子类型或未用同步原语保护时,就存在数据竞争——go run -race 会报 Data race。常见场景包括:
• 全局计数器(如 var count int)被多个 goroutine 自增
• map 被并发读写(即使只是 m[key] = value 和 val := m[key] 交替发生)
• 结构体字段被不同 goroutine 修改而无同步机制
• 使用 sync/atomic 但误用了非原子操作(比如对 int64 用 atomic.LoadInt32)
mutex 和 RWMutex 怎么选
sync.Mutex 是最常用的选择,适合读写都较频繁、或写操作不可忽略的场景;sync.RWMutex 在「读多写少」时更高效,但要注意:它不保证写操作的绝对优先级,可能被持续读请求饿死。
• 写操作占比 > 10%,直接用 Mutex
• 读操作远多于写(比如配置缓存、白名单 map),且写入极少(如启动时加载 + 运行时偶尔刷新),用 RWMutex
• 不要嵌套调用 RWMutex.RLock() 多次再 Unlock() —— 它不是可重入锁,重复 RLock() 没问题,但每个 RLock() 都要配对 RUnlock(),否则会死锁
• defer mu.Unlock() 必须紧跟在 mu.Lock() 或 mu.RLock() 后,否则可能漏解锁
map 并发读写错误怎么修
Go 的原生 map 不是并发安全的,运行时报错通常是:fatal error: concurrent map read and map write 或 concurrent map iteration and map write。
• 最简单修复:用 sync.RWMutex 包裹整个 map 操作
• 更轻量替代:用 sync.Map,但它只适合「低频写 + 高频读」且 key 类型为 string 或 int 等常见类型;注意:sync.Map 不支持遍历(range),必须用 Range() 方法传函数回调
• 切忌用 map + atomic.Value 手动包装——atomic.Value 只能存「不可变值」,map 本身是引用类型,替换整个 map 指针可以,但无法解决内部元素并发修改问题
var cache = struct {
mu sync.RWMutex
data map[string]int
}{data: make(map[string]int)}
func Get(key string) (int, bool) {
cache.mu.RLock()
defer cache.mu.RUnlock()
v, ok := cache.data[key]
return v, ok
}
func Set(key string, val int) {
cache.mu.Lock()
defer cache.mu.Unlock()
cache.data[key] = val
}
初学者最容易忽略的锁边界
锁的粒度和作用域比“加不加锁”更重要。新手常犯的错:
• 把锁放在函数开头、defer Unlock() 放在结尾,但中间有耗时操作(如 HTTP 请求、数据库查询),导致锁持有时间过长,成为性能瓶颈
• 在循环内反复加锁/解锁,而不是把整个循环包进临界区(如果逻辑允许)
• 锁保护了变量,却忘了保护其依赖状态——比如一个 isReady bool 和一个 data []byte,两个字段需同时更新,只锁其中一个没用
• 在方法接收者上用指针接收但忘记锁字段所属结构体实例(即锁对象和被锁数据不在同一内存上下文)








