Go 里 channel 和 mutex 该用哪个
给烧烤店做那个小程序的时候,后台有个当日订单统计,我一开始用 channel 把每笔订单喂给一个汇总 goroutine。跑了半天发现,读统计结果也得走 channel,代码绕了三层。所以 Go channel 和 mutex 该用哪个,我现在的判断标准是:传数据用 channel,护状态用 mutex。
一、Go channel 和 mutex 该用哪个,两个维度摆一起看
| 维度 | channel | mutex |
|---|---|---|
| 主要用途 | 在 goroutine 之间传递数据 | 保护一块共享状态 |
| 出问题时表现 | 卡住、死锁、goroutine 堆着不退 | 数据竞争,跑 -race 才报 |
| 代码可读性 | 数据流清楚,控制流绕 | 临界区一眼看得见 |
| 适合的数量级 | 少量 goroutine、清晰的流水线 | 大量读写同一份数据 |
| 我的使用比例 | 越来越少 | 越来越多 |
表格最后一行是真心话。刚开始写 Go 的时候我觉得 channel 高级,什么都要 channel 一下,现在反过来了。
二、channel 真正擅长的是什么
它擅长的是一条流水线:上游产出、下游消费,中间有明确的先后关系。
比如我那个视频管线,扫描素材、渲染、合成音轨这几步串起来,用 channel 接是最自然的写法。每一步只管自己那一段,谁也不用知道全局。
它不擅长的是「大家随便读随便写同一份数据」。那种场景硬套 channel,你等于自己手写了一个更慢的锁。
三、mutex 的正确用法就一条
锁住的状态,就只让锁保护它,别的地方不许碰。
我见过最常见的翻车是:加了锁,但读的时候忘了加。写的地方规规矩矩 Lock/Unlock,读的地方直接裸访问 map,跑起来大部分时候没事,压测一上来就崩。
Go 的并发读写 map 检测很严,一旦真并发写,进程会直接报错退出,不是返回一个错值。这条我在小程序后台吃过一次,日志里那行提示到现在还记得。
另一个细节是锁的粒度。锁里别干慢活——别在临界区里发 HTTP、别读文件、别做耗时计算。锁里只做内存操作,慢活挪到锁外面。
四、判断这件事,我用的三个问题
第一个问题:这份数据是谁的? 如果它有明确归属者,别的 goroutine 只是把活交给它干,那是 channel。如果它属于所有人共同读写,那是 mutex。
第二个问题:我要的是顺序还是互斥? 要保证 A 做完 B 才能做,channel 天然带这个语义。只要保证同一时刻只有一个人动它,mutex 更直接。
第三个问题:出 bug 的时候我想看到什么? channel 出问题通常表现为卡住,排查要 dump 所有 goroutine 的栈;mutex 出问题通常是数据竞争,跑一次带 -race 的测试就能定位。我个人更怕卡住,因为线上卡住是持续掉请求。
五、我现在的默认选择
新写一段代码,如果拿不准,我先上 mutex。
理由很实际:锁的边界是局部的,看不懂这一小块就行;channel 的边界是跨函数的,理解成本摊在整条调用链上。一个人维护的项目,局部的东西更容易改。
只有当我明确在搭一条流水线、或者要控制并发数量的时候,我才用 channel。这时候它省掉的代码量是实打实的。
顺带说一句,这两个东西经常要一起用:用 channel 传任务,用 mutex 保护那个任务要改的共享状态。所以 Go channel 和 mutex 该用哪个,答案从来不是二选一,是分工。
常见问题
Q:Go 里 channel 和 mutex 该用哪个,有没有一句话判断?
A:传数据用 channel,护状态用 mutex。拿不准的时候先上 mutex,因为它的边界更局部,以后好改。
Q:Go channel 和 mutex 该用哪个,用 channel 是不是更「Go」?
A:不是。官方那句「用通信共享内存」有前提,指的是数据所有权要转移的场景。单纯保护一份共享配置或者缓存,用锁更简单也更快。
Q:两个一起用会不会更容易死锁?
A:会,所以要定顺序。我的做法是锁只在最内层函数里出现,绝不跨 channel 收发持有锁。
相关阅读
- Go context 超时取消怎么一层层传下去
- Go 并发读写 map 报错,怎么改才安全
- Go 并发测试老是挂,我最后这么改稳的
- Go 并发编程的 5 个常见坑,新手先看
- WaitGroup 最容易写错的三种写法
本文作者西米,37 岁程序员,一个人加五个 AI 员工在跑一家公司。踩过的坑和真实账本都写在公众号「西米分享」。
完整代码、踩坑清单与最新进展,都在公众号。
📱 关注公众号「西米分享」