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 的 mutex - 锁粒度宁小勿大:不要整个函数都包在
mu.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 位系统上非原子)。
立即学习“go语言免费学习笔记(深入)”;
-
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 本质是通信机制,不是锁。它适合“传递所有权”或“协调生命周期”,比如生产者-消费者、信号通知、限流。但若只为保护一个计数器而开 channel,性能差、逻辑绕、还容易漏 close 或 goroutine 泄漏。
- 用 channel 做同步,通常意味着你把“状态变更”转成了“消息发送”,适合跨 goroutine 协作,不适合高频、细粒度的数据保护
- 带缓冲 channel(如
make(chan struct{}, 1))模拟互斥锁是可行的,但可读性差、调试难,远不如sync.Mutex直观 - 如果发现要用
select+default非阻塞尝试获取 channel,基本说明设计已偏离 Go 的 channel 原意
真正难的不是选哪个同步原语,而是识别出哪些变量是共享的、哪些访问路径是并发的——这需要结合调用栈和 goroutine 生命周期去分析,而不是看到 go f() 就给所有变量加锁。










