
本文详解如何用 Go 的 goroutine 和 channel 高效并发发起数千次 HTTP 请求,将原本 16 分钟的串行任务缩短至数秒,并提供可复用、可配置范围的并发函数模板。
本文详解如何用 go 的 goroutine 和 channel 高效并发发起数千次 http get 请求,将原本 16 分钟的串行任务缩短至数秒,并提供可复用、可配置范围的并发函数模板。
在构建城市公交站牌状态监控 API 时,需批量探测 1500+ 个 URL(如 http://www.urbanosdezaragoza.es/frm_esquemaparadatime.php?poste=XXX)的可达性。若采用串行 http.Get,耗时长达 16 分钟以上;而合理使用 Go 的并发原语——goroutine 与 channel——可将总耗时压缩至 2~5 秒(取决于目标服务响应速度与客户端并发限制),大幅提升效率。
✅ 核心思路:工作池 + 通道聚合
不推荐为每个请求启动一个 goroutine 后无节制等待(易触发连接耗尽或服务拒绝),而应:
- 使用固定数量的 goroutine 工作池(如 20~50 个)处理请求;
- 通过 channel 统一收集结果;
- 主 goroutine 控制任务分发与结果消费。
以下是一个生产就绪的简化实现(已适配你的实际需求):
package main
import (
"fmt"
"net/http"
"strconv"
"time"
)
// getBusPostStatus 并发检查 [start, end) 范围内的公交站牌 URL 状态
func getBusPostStatus(start, end int, results chan total {
endIdx = total
}
go getBusPostStatus(startIdx, endIdx, results)
}
// 收集全部结果(共 total 条)
for i := 0; i <h3>? 关键要点说明</h3>
-
http.Client复用:创建一次client实例并复用,避免重复初始化开销和连接管理混乱; -
resp.Body.Close()必须调用:否则底层 TCP 连接不会释放,导致too many open files错误; - 显式超时控制:防止某条慢请求拖垮整体进度;
-
channel 缓冲区:
make(chan string, total)提升吞吐,避免 sender 因 receiver 暂未读取而阻塞; -
任务分片策略:将 1500 个 ID 切分为多个
[start, end)区间,每个 goroutine 独立处理一段,逻辑清晰且易于扩展; -
并发数调优建议:初始设为
20~50;若目标服务器有速率限制,可降至5~10;若本地带宽充足且服务稳定,可尝试100+,但务必配合Client.Timeout和重试机制。
? 原代码问题回顾(供学习参考)
- 全局变量
i被多个 goroutine 共享并修改 → 竞态条件(race condition); -
threadPrint使用死循环for { ,但未控制退出条件 → 可能 panic 或无限等待; - 未关闭
resp.Body→ 连接泄漏,最终耗尽文件描述符; - 无超时、无错误重试、无并发数限制 → 不具备生产可用性。
? 进阶提示:真实项目中建议引入
errgroup(golang.org/x/sync/errgroup)统一错误传播,或使用sync.WaitGroup+slice替代 channel 收集结构化结果(如[]struct{ID int; Status string; Err error}),便于后续统计与导出。
通过以上改造,你不仅能快速完成 1500 次探测,更掌握了 Go 并发编程的核心范式:“不要通过共享内存来通信,而要通过通信来共享内存”。










