Go 并发核心在于理解 channel 阻塞语义、select 非抢占调度及 sync.Mutex 适用场景;需用 sync.WaitGroup 等同步机制避免主 goroutine 提前退出,防止循环变量复用导致数据错误,禁用 time.Sleep 做同步,避免 channel 读写不配对引发死锁,合理选择缓冲/无缓冲 channel,关闭 channel 前确保写端完成,select 随机选就绪分支且仅支持纯通信操作。

Go 并发不是靠“多开 goroutine”就能写对的,核心在于理解 channel 的阻塞语义、select 的非抢占式调度,以及何时该用 sync.Mutex 而非靠 channel 串行化。
goroutine 启动后就“消失”了?必须加同步机制
新手常以为 go f() 启动后函数会自然执行完,但主 goroutine 退出时整个程序立即终止,其他 goroutine 来不及执行。
- 用
sync.WaitGroup显式等待:调用wg.Add(1)在启动前,wg.Done()在 goroutine 结束时,主协程调用wg.Wait() - 避免在循环中直接启动 goroutine 并复用循环变量(如
for _, v := range items { go func() { println(v) }() }),会导致所有 goroutine 看到同一个v的最终值;应传参:go func(val string) { println(val) }(v) -
time.Sleep不是同步手段,仅用于调试;生产代码中它掩盖了竞态,且无法保证等待足够久
channel 读写不配对就会死锁
向无缓冲 channel 发送数据会阻塞,直到有 goroutine 准备接收;若发送方和接收方没对齐(比如只发不收、或只收不发),fatal error: all goroutines are asleep - deadlock! 立刻出现。
- 无缓冲 channel(
make(chan int))适合严格的一对一同步;有缓冲 channel(make(chan int, 10))可缓解生产者/消费者速率差异,但缓冲区满后仍会阻塞发送 - 从已关闭的 channel 读取会立即返回零值;向已关闭的 channel 写入 panic,所以关闭前确保所有写端都已完成
- 用
select配合default可实现非阻塞尝试读/写,避免卡住
select 是并发控制枢纽,不是 switch 的并发版
select 会在多个 channel 操作中**随机选择一个就绪的分支**执行,没有优先级,也没有“条件判断”逻辑——它只看 channel 是否可读/可写。
- 所有 case 中的 channel 操作必须是纯通信动作(如
ch 或),不能带函数调用或赋值表达式 - 如果多个 case 同时就绪,Go 运行时随机选一个,不可预测;不要依赖顺序
- 想实现超时,用
time.After(d)构造的 channel;想避免阻塞,加default:分支 - 空
select{}会永久阻塞,等价于for {},常用于让主 goroutine 等待信号
select {
case msg := <-ch:
fmt.Println("received", msg)
case <-time.After(2 * time.Second):
fmt.Println("timeout")
default:
fmt.Println("no message ready")
}sync.Mutex 和 channel 不是二选一,而是分工明确
channel 用于 goroutine 间**传递数据与控制流**;sync.Mutex 用于保护**共享内存的临界区**。混用或误用会导致隐蔽 bug 或性能瓶颈。
- 当多个 goroutine 需要读写同一结构体字段(如计数器、缓存 map)、且操作不是原子的,必须用
mu.Lock()/Unlock()包裹,不能指望 channel 转发来“串行化”——那只是把竞争转移到了 channel 上 -
sync.RWMutex适合读多写少场景,允许多个 reader 并发,但 writer 仍独占 - 避免在持有 mutex 时调用可能阻塞或长时间运行的函数(如网络请求、channel 操作),否则会拖慢所有等待该锁的 goroutine
真正难的不是写出能跑的并发代码,而是判断某个状态是否被多个 goroutine 共享、哪些操作必须原子、channel 边界是否清晰——这些没法靠抄例子解决,得在 debug 竞态(go run -race)和重读 runtime.gopark 行为中慢慢建立直觉。










