Go并发共享数据必须用同步机制防data race;sync.Mutex保护临界区,注意非重入、锁粒度和defer时机;sync.RWMutex适合读多写少;sync/atomic用于基础类型无锁操作;channel本质是通信而非锁,不宜滥用。
Go 语言并发下共享数据,不能靠“约定”或“自觉”,必须用同步机制;否则 data race 是大概率事件,且可能在压测或上线后才暴露。
sync.Mutex 保护临界区最直接只要多个 goroutine 会读写同一变量(比如一个 map 或结构体字段),就必须加锁。注意:只读一般不需锁,但若读操作与写操作并发,且该读发生在写中间(如遍历 map 同时有 delete),仍可能 panic。
sync.Mutex 是非重入锁,同一个 goroutine 重复 Lock() 会死锁defer mu.Unlock(),但要确保 mu.Lock() 已执行,否则 defer 会 unlock 未 lock 的 mutexmu.Lock() / mu.Unlock() 里,只包真正访问共享数据的几行var mu sync.Mutex
var counter int
func increment() {
mu.Lock()
counter++
mu.Unlock()
}
sync.RWMutex 适合读多写少场景当共享数据被频繁读取、极少修改(如配置缓存、白名单列表),sync.RWMutex 能显著提升并发吞吐。多个 goroutine 可同时持有读锁,但写锁会阻塞所有读写。
RUnlock() 必须和 RLock() 配对,漏掉会导致后续写锁永远无法获取Rlock 再想 Lock)——这会死锁,必须先 RUnlock() 再 Lock()
var rwmu sync.RWMutex
var config map[string]string
func getConfig(key string) string {
rwmu.RLock()
defer rwmu.RUnlock()
return config[key]
}
func updateConfig(k, v string) {
rwmu.Lock()
defer rwmu.Unlock()
config[k] = v
}
sync/atomic 处理简单整数或指针的无锁更新仅适用于基础类型:int32、int64、uint32、uint64、uintptr 和 *T。不是所有字段都能原子化,比如 struct 字段或 int(在 32 位系统上非原子)。
atomic.LoadInt64(&x) 和 atomic.StoreInt64(&x, v) 是最常用组合atomic,另一部分直接读写变量,那等于没保护atomic.AddInt64 返回新值,适合计数器累加后判断阈值var hits int64
func recordHit() {
atomic.AddInt64(&hits, 1)
}
func getHits() int64 {
return atomic.LoadInt64(&hits)
}
channel 本质是通信机制,不是锁。它适合“传递所有权”或“协调生命周期”,比如生产者-消费者、信号通知、限流。但若只为保护一个计数器而开 channel,性能差、逻辑绕、还容易漏 close 或 goroutine 泄漏。
make(chan struct{}, 1))模拟互斥锁是可行的,但可读性差、调试难,远不如 sync.Mutex 直观select + default 非阻塞尝试获取 channel,基本说明设计已偏离 Go 的 channel 原意真正难的不是选哪个同步原语,而是识别出哪些变量是共享的、哪些访问路径是并发的——这需要结合调用栈和 goroutine 生命周期去

go f() 就给所有变量加锁。