Go 的并发模型以 goroutine 和 channel 为核心,写法简洁,但也容易踩坑。下面梳理几类常见误用,并给出可验证的修正思路。
1. 忘记等待 goroutine 结束
go f() 启动后不会阻塞主流程。如果 main 函数直接返回,未执行完的 goroutine 会被直接终止,输出可能不完整。
修正方式:
- 用
sync.WaitGroup:wg.Add(1)放在启动 goroutine 之前,goroutine 内defer wg.Done(),主流程wg.Wait()。 - 用带缓冲的 channel 收集结果,读取预期数量的值后再继续。
注意:wg.Add 写在 goroutine 内部可能与 wg.Wait 产生竞态,是常见错误。
2. 向无缓冲 channel 发送却无人接收
无缓冲 channel 的发送和接收必须同时就绪。若只有一个 goroutine 向无缓冲 channel 发送,而接收方迟迟未启动或已退出,发送方会永久阻塞,进而导致整个程序死锁。
修正方式:
- 明确接收方一定存在,或改用带缓冲 channel 并设置合理容量。
- 用
select配合default或time.After做超时兜底。
3. 只发送不关闭,range 永不结束
for v := range ch 只有在 channel 被关闭后才会退出。如果发送方忘记 close(ch),循环会一直阻塞等待。
修正方式:
- 由发送方在发送完成后调用
close(ch);不要在接收方关闭。 - 向已关闭的 channel 发送会 panic,因此要确保关闭只发生一次。
4. 用 channel 传共享变量,而不是传所有权
Go 的惯例是“通过通信共享内存,而不是通过共享内存通信”。若多个 goroutine 直接读写同一个 map 或 slice,即使有 channel 参与,也可能触发 data race。
修正方式:
- 用
go run -race或go test -race检测竞态。 - 要么让数据只在一个 goroutine 内被修改,要么用
sync.Mutex、sync.RWMutex保护。
5. goroutine 泄漏
启动的 goroutine 因阻塞在 channel 收发、锁等待或网络 IO 上而永远无法退出,就是 goroutine 泄漏。数量持续增长会耗尽内存。
修正方式:
- 为阻塞操作设置超时或
context.Context取消信号。 - 在
select中监听ctx.Done(),收到取消后及时返回。
select {
case v := <-ch:
handle(v)
case <-ctx.Done():
return
}
6. 误用 nil channel
对 nil channel 的发送和接收都会永久阻塞,但用于 select 时可以动态启用或禁用分支。若不小心把 nil channel 当作普通 channel 使用,程序会卡住。
修正方式:初始化后再使用,或在 select 中利用 nil channel 屏蔽某个分支。
7. 把无缓冲 channel 当同步队列
无缓冲 channel 容量为 0,发送和接收严格配对,吞吐通常低于带缓冲 channel。若生产者速度快于消费者,无缓冲 channel 会频繁阻塞。
修正方式:根据生产/消费速率差设置缓冲大小,或引入批量处理。缓冲大小需要实测,不能凭感觉设定。
小结
- 启动 goroutine 前先想清楚它何时结束。
- channel 由发送方关闭,只关闭一次。
- 用
-race检测共享内存竞态。 - 用
context和超时避免永久阻塞与泄漏。
这些做法都能通过 go vet、go test -race 和实际运行验证,建议在项目中固定为代码审查清单。