循环启动多Go协程后,如何正确回收所有协程的执行结果?
这种协程+Channel收不到结果的坑我之前踩过好多次,大概率是几个常见的细节没处理对,咱们一步步拆解问题,给出最佳方案:
常见问题排查&解决方法
1. Channel是局部变量,协程间无法共享
如果你的getScore函数里自己创建了局部Channel,那每个协程的数据都发到了私有Channel里,主goroutine根本拿不到!这是最常见的错误之一。
错误示例:
func getScore() { c := make(chan ScoreResult) // 每个协程专属的Channel,主goroutine访问不到 // ...执行HTTP请求、计算res c <- res }
正确做法:把同一个Channel作为参数传给所有协程,让它们都往同一个Channel里发结果:
// 定义getScore时,接收一个发送方向的Channel func getScore(c chan<- ScoreResult, url string) { // ...执行HTTP请求、处理错误、计算res c <- res // 往共享的Channel发结果 } // 主goroutine中创建共享Channel,传入每个协程 func main() { urls := []string{"url1", "url2", "url3"} ch := make(chan ScoreResult, len(urls)) // 带缓冲,大小等于协程数,避免发送阻塞 for _, u := range urls { go getScore(ch, u) } // 后续接收逻辑... }
2. 无缓冲Channel导致发送阻塞
如果用的是无缓冲Channel(make(chan T)),协程往Channel发数据时会阻塞到有接收者准备好接收。如果你的主goroutine是先启动所有协程,再开始接收,那所有协程都会卡在发送步骤,自然Channel里没数据。
解决方法二选一:
- 给Channel设置缓冲,缓冲大小等于协程数量(如上面的示例),这样协程可以直接把数据写入缓冲,不用等待接收
- 主goroutine在启动协程的同时就开始接收(比如用
for range或者select)
3. 等待协程的方式不正确
如果你的“等待所有协程执行完毕”用的是time.Sleep,那完全不可靠——协程执行时间可能超过sleep时长,导致还没发送数据就被主goroutine提前结束了。
最佳实践:用sync.WaitGroup+关闭Channel
WaitGroup用来等待所有协程完成,完成后关闭Channel,主goroutine就可以通过for range遍历所有结果:
import "sync" type ScoreResult struct { URL string Score int Err error } func main() { urls := []string{"url1", "url2", "url3"} ch := make(chan ScoreResult, len(urls)) var wg sync.WaitGroup for _, u := range urls { wg.Add(1) // 注意用闭包捕获当前的u,避免循环变量复用问题 go func(url string) { defer wg.Done() // 协程结束时标记完成,即使出错也能执行 res, err := doHTTPAndCalculate(url) // 替换成你的getScore逻辑 // 无论成功失败,都往Channel发结果,避免主goroutine一直等 ch <- ScoreResult{ URL: url, Score: res, Err: err, } }(u) } // 启动单独协程,等所有任务完成后关闭Channel go func() { wg.Wait() close(ch) }() // 主goroutine接收所有结果 for result := range ch { if result.Err != nil { // 处理错误 println("处理", result.URL, "失败:", result.Err.Error()) continue } println(result.URL, "的得分是:", result.Score) } }
4. 忽略错误导致未发送数据
如果getScore里的HTTP请求失败了,你却没有处理错误,直接跳过了发送数据的逻辑,那这个协程的结果就不会进入Channel,导致Channel里的数据数量比协程数少,甚至为空。
一定要处理所有错误路径:即使请求失败,也要往Channel发送一个包含错误的结果,或者至少在日志里记录,这样你才能知道哪里出了问题,而不是疑惑为什么Channel为空。
总结最佳方案
- 使用共享的带缓冲Channel,缓冲大小等于协程数量,避免发送阻塞
- 用
sync.WaitGroup管理协程生命周期,配合单独的协程关闭Channel - 确保每个协程无论成功失败都会向Channel发送数据,避免主goroutine挂起或数据丢失
- 不要用
time.Sleep等待协程,这是极不可靠的做法
内容的提问来源于stack exchange,提问作者Enrique Alcazar

