应根据操作对象选择:共享内存用 sync.Mutex,goroutine 通信用 chan;改 map 或字段必须用 Mutex,分发任务或通知必须用 chan。

该用 sync.Mutex 还是 chan?看操作对象是不是“共享内存”
锁和 channel 不是同类工具:sync.Mutex 是为保护同一块内存而生的,chan 是为 goroutine 之间传递消息而设计的。选错就等于用扳手拧螺丝——能动,但费劲还易坏。
- 如果你在改一个
map[string]int、递增一个count字段、更新结构体里的状态标志——必须用sync.Mutex或sync.RWMutex,否则直接触发fatal error: concurrent map writes或数据错乱 - 如果你在分发任务、等待某个结果、通知“配置已热更”、实现超时退出——这时候
chan不是选项,是唯一自然解法 - 常见误用:开个
chan struct{}容量为 1,每次操作前select { case sem ,完事再 <code><-sem。这本质是用 channel 模拟锁,调度开销大、语义模糊、容易死锁 - 性能上,
mu.Lock()/mu.Unlock()是纳秒级;无缓冲chan的收发至少微秒级,跨 goroutine 时更重
读多写少场景下,优先考虑 sync.RWMutex 而不是普通 sync.Mutex
比如缓存、配置快照、服务健康状态这类数据,95% 时间只读,只有后台定时刷新或热更新才写——这时用 sync.RWMutex 能显著提升并发吞吐。
-
RWMutex.RLock()允许多个 goroutine 同时读,不互斥;RWMutex.Lock()写时会阻塞所有新读/写,保证独占 - 别把整个 handler 方法包进
mu.Lock():锁粒度太粗,HTTP 请求一卡,所有请求排队等 - 避免在锁内做阻塞操作:
http.Get、db.Query、time.Sleep都不该出现在Lock()和Unlock()之间,否则拖垮所有等待者 - 如果只是单个整数计数器,且只需增减/读取,
sync/atomic比锁更轻:用atomic.AddInt64(&counter, 1),零分配、无调度、无锁
channel 必须用对场景,否则就是给自己埋雷
channel 不是用来“同步访问变量”的,它是用来表达“谁提供、谁消费、谁通知、谁退出”的协作关系。滥用它,代码会越来越难推理。
- worker pool 分发任务、日志收集 pipeline、信号广播(如
done chan struct{})、配合select做超时控制——这些是 channel 的主场 - 向已关闭的
chan发送数据会 panic;从已关闭的chan接收会得到零值并继续执行;但反复close同一个 channel 也会 panic - 别用
chan int来“传最新值”,比如轮询式地想拿到当前计数器——这是状态同步,该用原子操作或带锁字段,而不是靠 channel 收发来模拟 - 创建 channel 时明确所有权:生产者负责
close(),消费者绝不 close;用chan<-或<-chan标注方向,防误写
mutex 和 channel 可以组合,但得清楚各自职责
真实业务里常要“安全改状态 + 通知别人”,这时候不是二选一,而是各司其职:锁管内存安全,channel 管通信解耦。
立即学习“go语言免费学习笔记(深入)”;
- 示例:缓存更新后通知监控模块。用
mu.Lock()保护map写入,再往updateCh chan string发一条 key ——注意加缓冲(如make(chan string, 10)),避免写通知时阻塞主逻辑 - 监听方用
for key := range updateCh,不关心谁写的,只响应事件;发送方也不关心谁在收,只管发 - 切忌把 channel 放进锁里等接收:比如在
mu.Lock()里select { case updateCh ,万一没人收,就卡死整个临界区 - 初始化一次性动作(如加载配置、连 DB)首选
sync.Once,不是起 goroutine +done chan bool:后者失败时可能永远不写,调用方永久阻塞
最常被忽略的一点:没有“万能选择”。看到 map 就想到锁,看到“任务来了”就想到 channel,看到“只读一个 int”就想到 atomic——这种直觉需要建立在理解“问题本质是状态保护还是流程协作”的基础上。写多了就会发现,真正难的不是语法,而是第一时间判断出那个“本质”。










