
本文介绍 Go 并发编程中协调 sync.WaitGroup 和 channel 的最佳实践,重点解决“何时关闭通道”这一常见陷阱,推荐使用独立的 done 通道配合 select 实现无竞态、可预测的 goroutine 终止。
本文介绍 go 并发编程中协调 `sync.waitgroup` 和 channel 的最佳实践,重点解决“何时关闭通道”这一常见陷阱,推荐使用独立的 done 通道配合 `select` 实现无竞态、可预测的 goroutine 终止。
在 Go 中,将 sync.WaitGroup 与 channel 混合使用时,一个典型误区是:依赖另一个 goroutine 等待 WaitGroup 完成后再关闭 channel(如原代码中 go func(){ waitGroup.Wait(); close(ch) }())。这种做法不仅引入额外 goroutine 增加复杂度,更存在严重竞态风险——若所有 worker 已提前退出但 WaitGroup 尚未归零,主循环可能永远阻塞在 range ch;反之,若 channel 过早关闭,后续发送将 panic。
✅ 更安全、更符合 Go 习惯的做法是:用一个专用的 done 通道(通常为 chan struct{})显式通知消费端“不再有新消息”,并配合 select 非阻塞监听数据与终止信号。
以下是重构后的推荐实现:
func Crawl(url string, depth int, fetcher Fetcher) {
ch := make(chan string, 10) // 建议设缓冲,避免 sender 阻塞
done := make(chan struct{}) // 专用终止信号通道
var wg sync.WaitGroup
wg.Add(1)
go crawlTask(&wg, ch, done, url, depth, fetcher)
// 主循环:用 select 替代 range,响应数据或 done 信号
for {
select {
case msg, ok := <p>? 关键改进说明:</p>
-
done通道单向只读()传入 <code>crawlTask,确保仅由 worker 决定何时关闭,主循环只监听; -
select保证响应及时性:无论消息到达还是终止信号触发,都能立即处理,无死锁风险; -
关闭
done即代表“全部任务完成”,逻辑清晰,无需等待WaitGroup归零(WaitGroup仅用于内部 goroutine 生命周期管理); -
所有向
ch发送操作均用select+done检查,防止在done触发后仍尝试写入已失效的 channel; -
done使用struct{}类型:零内存开销,语义明确(仅作信号,不传数据)。
⚠️ 注意事项:
- 切勿在多个 goroutine 中重复
close(done),应确保仅由最后一个完成的 worker 调用(本例中depth 是递归基线,天然满足); - 若需支持取消(如超时、用户中断),可将
done替换为context.Context.Done(),进一步提升健壮性; -
range ch仅适用于“生产者明确关闭 channel”的场景;当生产者生命周期不确定(如动态递归启动)时,select+done是唯一可靠方案。
总结:Go 的并发哲学强调“通过通信共享内存”,而通道就是通信的基石。与其让 goroutine 被动等待 WaitGroup,不如主动发送控制信号——用 done 通道解耦生命周期管理与数据流,让同步逻辑清晰、可测试、无竞态。










