
1. 前言Goroutine 是 Go 语言最亮眼的特性轻量协程让并发代码写起来很方便。但很多新手写多协程代码时会遇到一个诡异问题明明逻辑看着没问题多次运行结果却不一样。这就是竞态条件而sync包里的锁就是用来解决这个问题的。网上不少讲解 sync.Mutex 和 RWMutex 的文章一上来就切入底层状态机、源码实现刚学 Go 的同学很容易看得头大。这篇文章我们不从底层源码开始而是从问题出发用极简的代码例子搞懂两件事互斥锁 Mutex 到底做了什么怎么正确使用会踩哪些坑读写锁 RWMutex 和普通互斥锁的区别什么时候才该选用读写锁读完之后你能分清两种锁并且知道在业务代码里怎么选避开新手高频 bug。前置基础知道什么是 goroutine简单了解并发、竞态条件。2. 先搞懂为什么需要锁竞态条件是什么先看一个计数器示例多个 goroutine 同时修改同一个变量结果和预期不一致。package main import ( fmt time ) func main() { cnt : 0 for i : 0; i 1000; i { go func() { cnt }() } time.Sleep(100 * time.Millisecond) fmt.Println(cnt , cnt) }多次运行你会发现输出不是固定 1000每次数值不一样。原因cnt不是原子操作会拆成「读取值、1、写回」三步多个协程同时执行就会互相覆盖出现数据丢失。这就是竞态条件。想要修复就要加锁。一句话总结多个协程同时读写同一份共享数据就会产生竞态锁就是用来保护共享资源的工具。3. sync.Mutex 互斥锁最简单的排他锁核心特性同一时间只能有一个 goroutine 拿到锁不管读还是写拿到锁才能操作共享变量操作完解锁其他协程才有机会获取锁。两个核心方法mu.Lock()尝试获取锁如果锁已经被别人拿着当前 goroutine 阻塞等待mu.Unlock()释放锁其他等待的协程可以竞争获取最佳实践用defer mu.Unlock()保证即使临界区代码 panic锁也一定会释放避免死锁。下面用 Mutex 修复上面的计数器完整可运行package main import ( fmt sync time ) func main() { var mu sync.Mutex cnt : 0 for i : 0; i 1000; i { go func() { mu.Lock() cnt mu.Unlock() }() } time.Sleep(100 * time.Millisecond) fmt.Println(cnt , cnt) }多次运行输出稳定等于1000竞态问题消失。关键要点Unlock 必须执行推荐defer mu.Unlock()。禁止锁复制把 Mutex 当成值拷贝会复制一把全新未上锁的锁防护直接失效。常见坑忘记解锁死锁、重复解锁panic。结构体里带 Mutex传参一律用指针。❌ 错误示例type Counter struct { mu sync.Mutex cnt int } // 传值锁被拷贝防护失效 func (c Counter) Add() { c.mu.Lock() defer c.mu.Unlock() c.cnt }不要在Lock()之前写defer mu.Unlock()// ❌ 错误还没拿到锁就注册了解锁运行直接panic defer mu.Unlock() mu.Lock()defer 的执行逻辑defer的作用注册一个函数等到当前函数退出的时候才执行但注册动作是立刻发生的。代码执行顺序执行defer mu.Unlock()→立刻注册函数结束时调用mu.Unlock()然后执行mu.Lock()尝试获取锁有可能阻塞拿不到锁重复 Unlock 会直接 panic同一个 goroutine 重复 Lock 直接死锁。4. sync.RWMutex 读写锁读多写少场景优化核心规则sync.RWMutex读写锁把操作分成读锁 RLock ()/RUnlock ()和写锁 Lock ()/Unlock ()规则记住这 4 条就够✅读读并行多个 goroutine 可以同时拿到读锁一起读数据提升并发读性能❌读写互斥有协程持有读锁写锁需要等待有写锁读锁要等待❌写写互斥同一时间只能一个协程拿写锁写锁优先级较高申请写锁到来后后续新来的读锁会被阻塞防止写饥饿适用场景读多写少比如本地缓存、配置信息大量查询修改操作很少。下面用 RWMutex 实现一个简单的配置缓存对比 Mutex 看读多场景下的性能优势package main import ( fmt sync ) type Config struct { mu sync.RWMutex data map[string]string } func (c *Config) Get(key string) string { c.mu.RLock() defer c.mu.RUnlock() return c.data[key] } func (c *Config) Set(key, value string) { c.mu.Lock() defer c.mu.Unlock() c.data[key] value } func main() { cfg : Config{data: make(map[string]string)} cfg.Set(theme, dark) fmt.Println(theme , cfg.Get(theme)) }误区不是所有场景都用 RWMutex写频繁场景RWMutex 开销反而大于普通 Mutex。RWMutex 常见误区❌ 不是万能加速写频繁场景不要上 RWMutex。读写锁内部逻辑更复杂写多的时候开销比普通 Mutex 更大性能反而变差。❌ 读锁只是保护读读的时候如果要修改数据必须上写锁。配套成对使用RLock()必须搭配RUnlock()不能混用Lock/Unlock。5. Mutex vs RWMutex 快速对比表锁规则适用场景sync.Mutex读写互斥同一时间仅一个协程访问读写频率差不多或者写操作频繁sync.RWMutex读读并行读写互斥、写写互斥读多写少6. 高频踩坑合集重点博客收藏点建议锁保护范围尽量缩小只包裹读写共享变量的那一小段临界区代码。defer Unlock 顺序错误必须先 Lock再注册 defer 解锁顺序颠倒直接 panic锁拷贝结构体包含锁方法接收者不用指针锁拷贝导致并发保护失效死锁场景同一个 goroutine 重复 Lock协程 A 持有锁等待 B协程 B 持有锁等待 A循环等待RWMutex 滥用写操作并不少强行换成读写锁增加额外开销收益很小锁粒度太大一把大锁包裹大量无关业务逻辑并发直接退化成串行失去 goroutine 并发优势7. 什么时候尽量不要用锁Go 设计理念不要通过共享内存通信通过通信共享内存。简单说优先用 channel 做协程之间消息传递而不是直接共享变量加锁。举例子简单计数器可以用 channel 来实现完全规避竞态。package main import ( fmt ) func main() { ch : make(chan int, 1) done : make(chan struct{}) go func() { cnt : 0 for i : 0; i 1000; i { cnt } ch - cnt close(done) }() -done fmt.Println(cnt , -ch) }思路channel 作为唯一通道所有协程只能通过 channel 来读写计数天然串行化访问共享数据消除竞态。当然 channel 也有适用边界复杂状态管理场景锁依然是更合适的选择。锁是兜底方案不是并发首选方案。8. 小结sync.Mutex排他互斥锁简单可靠是并发保护的通用首选。读写差不多、写频繁优先选它。sync.RWMutex读写锁仅读多写少场景才有性能收益不要盲目使用。锁的核心目的保护共享变量消除竞态条件。编码准则锁粒度越小越好用完尽快释放永远不要拷贝锁。Go 并发哲学优先 channel锁作为兜底方案。9. 附录完整源码汇总可直接复制运行下面是本文所有示例代码的汇总方便读者直接复制运行、修改测试。9.1 无锁计数器错误示例package main import ( fmt time ) func main() { cnt : 0 for i : 0; i 1000; i { go func() { cnt }() } time.Sleep(100 * time.Millisecond) fmt.Println(cnt , cnt) }9.2 Mutex 修复计数器package main import ( fmt sync time ) func main() { var mu sync.Mutex cnt : 0 for i : 0; i 1000; i { go func() { mu.Lock() cnt mu.Unlock() }() } time.Sleep(100 * time.Millisecond) fmt.Println(cnt , cnt) }9.3 RWMutex 配置缓存package main import ( fmt sync ) type Config struct { mu sync.RWMutex data map[string]string } func (c *Config) Get(key string) string { c.mu.RLock() defer c.mu.RUnlock() return c.data[key] } func (c *Config) Set(key, value string) { c.mu.Lock() defer c.mu.Unlock() c.data[key] value } func main() { cfg : Config{data: make(map[string]string)} cfg.Set(theme, dark) fmt.Println(theme , cfg.Get(theme)) }9.4 Channel 实现计数器无锁方案package main import ( fmt ) func main() { ch : make(chan int, 1) done : make(chan struct{}) go func() { cnt : 0 for i : 0; i 1000; i { cnt } ch - cnt close(done) }() -done fmt.Println(cnt , -ch) }你写 Go 并发时踩过锁的坑吗死锁、锁复制、defer 顺序错误这类问题都可以在评论聊聊。收藏本文可在评论区留下疑惑将为你更新 sync 包疑惑的通俗讲解。