应根据操作对象选择:改共享内存(如map、字段、结构体状态)必须用sync.mutex或sync.rwmutex,传消息(如分发任务、通知状态、超时退出)必须用chan;读多写少场景优先用sync.rwmutex,锁粒度要细,禁在锁内做阻塞操作。

该用 sync.Mutex 还是 chan,不看“并发”这个词本身,而看你在操作什么:改共享内存就用锁,传消息就用 channel。
改 map、字段、结构体状态?必须用 sync.Mutex 或 sync.RWMutex
Go 不允许并发写 map,也不保证结构体字段读写原子性。一旦多个 goroutine 同时执行 m["key"] = val 或 obj.count++,直接触发 fatal error: concurrent map writes 或静默数据错乱。
- 哪怕只是递增一个计数器,也得加锁——除非你改用
atomic.AddInt64 - 读多写少场景(如配置缓存、服务健康状态),优先选
sync.RWMutex:读不互斥,写独占 - 别把整个 HTTP handler 包进
mu.Lock();锁粒度越细越好,只包真正共享的那几行 - 绝对不要在锁里调
http.Get、db.Query、time.Sleep——会卡住所有等待者
分发任务、通知状态、等结果、超时退出?用 chan 是自然解法
channel 的语义是“谁提供、谁消费、谁退出”。它不是为保护变量设计的,而是为表达协作关系。
- worker pool 用
chan Job分发任务,chan Result收集结果 - 热更新配置后广播通知,用
done chan struct{}或notify chan bool - 配合
select实现超时:select { case - 用
chan struct{}做信号量时,别开容量为 1 的 channel 来“模拟锁”——这是反模式,调度重、易死锁
性能和底层成本差异不能忽略
mu.Lock() 是纳秒级原子操作;无缓冲 chan 收发至少微秒级,且涉及 goroutine 切换、唤醒逻辑、内部锁(没错,channel 底层其实也用了 sync.Mutex)。
- 基准测试里,
sync.RWMutex.RLock()并发读吞吐远高于无缓冲 channel 轮询 - channel 是带调度开销的通信原语,不是轻量同步开关
- 向已关闭的
chan发送数据会 panic;反复close同一个chan也会 panic - 用
chan 或 <code> 标注方向,编译期就能防误写
别混淆“状态同步”和“消息传递”
想拿到“当前最新值”?比如轮询式地取某个计数器——这不是 channel 的职责。这是状态同步问题,该用原子操作或带锁字段。
-
chan int不适合做“最新值快照”,因为收发是事件驱动,不是状态反射 - Context 底层同时用了
chan(广播取消)和mutex(线程安全访问字段),说明两者本就是互补而非替代 - 最危险的误用:用 channel 模拟锁、用 mutex 实现 goroutine 协作流——语义错位,后期维护成本指数上升
真正难的是判断“这算不算共享内存”。比如一个 struct 里既有要并发读写的字段,又有只读的嵌套配置;这时候往往得拆开处理:字段用 sync.RWMutex,配置变更用 chan Config 通知。










