最稳方案是带缓冲的chan result+sync.waitgroup+单独关闭协程;并发append切片会因扩容引发运行时崩溃,必须用channel传递结果并统一处理错误。

直接用带缓冲的 chan Result + sync.WaitGroup + 单独关闭协程,是最稳、最易维护的方案;别用全局 slice 或 map 收结果,那会 panic。
为什么不能直接往 slice 里 append?
多个 goroutine 并发写同一个 []Result,只要触发底层数组扩容(比如从 4 扩到 8),就会出现两个 goroutine 同时写新数组头指针——Go 运行时直接报 fatal error: concurrent slice writes。这不是竞态检测警告,是运行时崩溃。
- 错误写法:
results = append(results, r)在每个 goroutine 里调用 - 正确思路:谁写、谁读、谁关 channel,边界必须清晰
- 缓冲大小建议设为任务总数:
make(chan Result, len(tasks)),避免 sender 阻塞
怎么安全收结果并处理错误?
每个 worker 必须返回结构体,把数据和错误都带上,主协程统一判错,而不是靠 panic 或忽略 err != nil。
- 定义:
type Result struct { Data []byte; Err error; Index int }(加Index可保留顺序) - worker 内必须
defer wg.Done(),防止fetch()panic 导致wg.Wait()永远不返回 - 主协程用
for r := range results收,range自动等 channel 关闭,不用数个数 - 错误处理别只看
r.Err != nil,HTTP 场景还要检查resp.StatusCode和io.ReadAll(resp.Body)是否成功
channel 数量多时,fan-in 怎么写才不卡死?
如果源不是固定几个 HTTP 请求,而是动态创建的 N 个 channel(比如从 Kafka 分区、gRPC 流、文件句柄轮询而来),用 select 轮询所有 channel 效率低且难维护;应该为每个输入 channel 启一个转发 goroutine,统一写进一个输出 channel。
- 关键点:
go func(ch - 必须确保所有输入 channel 最终会被关闭,否则
for r := range ch会永远阻塞 - 输出 channel 的关闭时机:所有转发 goroutine 启动完后,另起一个 goroutine 等它们全部退出再
close(out) - 别在转发 goroutine 里
close(out),那会导致第一个退出的就关了整个出口
真正容易被忽略的是:所有 channel 操作都要配合 context.Context 做取消。比如某个 gRPC 流卡住没发完,光关输入 channel 不管用,得在 Recv() 时传入带 cancel 的 ctx,否则转发 goroutine 就悬在那里,内存和 goroutine 都不释放。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











