应使用 context.WithTimeout 为每个 HTTP 请求注入超时控制并调用 cancel(),配合 chan struct{} 实现并发数限制;http.Client.Timeout 无法中断卡死读取,仅 context 可真正可控超时。

用 http.Client 配合 context.WithTimeout 控制单个请求超时
并发发请求时,不加超时控制极易导致 goroutine 泄漏或服务雪崩。Go 标准库的 http.Client 本身不自动超时,必须显式通过 context 注入。
- 每次请求前创建带超时的
context.Context,例如ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) - 把
ctx传给http.NewRequestWithContext(ctx, ...),而非旧版的http.NewRequest - 务必调用
cancel()(哪怕请求已完成),避免上下文泄漏;可用defer cancel()确保执行 - 注意:
http.Client.Timeout字段只作用于整个请求生命周期(连接+读写),无法中断已建立连接后的卡死读取;用context才真正可控
用 semaphore(信号量)实现并发请求数限制
直接启动成百上千 goroutine 调用 API,容易打爆下游或触发限流。Go 没有内置信号量,但可用 chan struct{} 快速实现轻量级计数型限流。
- 初始化一个带缓冲的 channel:
sem := make(chan struct{}, 10)表示最多 10 个并发 - 每个请求前执行
sem (阻塞直到有空位) - 请求结束后(无论成功失败)执行
归还配额,建议用defer func() { - 别用
sync.WaitGroup替代——它不控制并发度,只做等待;也别依赖runtime.GOMAXPROCS,它管的是 OS 线程,不是业务并发数
批量请求失败时如何聚合错误并保留响应体
并发请求中部分失败很常见,但默认丢掉响应体或只返回第一个错误,会丢失调试关键信息。
- 为每个请求单独捕获
error和*http.Response,不要在 goroutine 外部共享变量存结果 - 用结构体封装结果:
type Result struct { URL string; Status int; Body []byte; Err error } - 对
Resp.Body要及时io.ReadAll并resp.Body.Close(),否则连接不会复用,且内存泄漏 - 若需区分网络错误、超时、HTTP 状态码错误,检查
err类型(如url.Error)、resp.StatusCode,而不是只看err != nil
用 net/http.Transport 复用连接与控制底层行为
高频并发请求下,默认 http.Transport 的连接池参数往往不够用,容易出现 dial tcp: lookup failed 或 connection refused。
立即学习“go语言免费学习笔记(深入)”;
- 自定义
http.Client并设置Transport:client := &http.Client{Transport: &http.Transport{...}} - 关键参数:
MaxIdleConns: 100(全局最大空闲连接)MaxIdleConnsPerHost: 100(每 host 最大空闲连接)IdleConnTimeout: 30 * time.Second(空闲连接存活时间)TLSHandshakeTimeout: 5 * time.Second(TLS 握手超时) - 不建议设
DisableKeepAlives: true,这会让每次请求都重建 TCP 连接,性能断崖下跌 - 若目标 API 支持 HTTP/2,确保 Go 版本 ≥ 1.6 且服务端配置正确;HTTP/2 默认启用多路复用,能显著降低连接开销
实际跑起来你会发现,最常被忽略的是 Body.Close() 和 cancel() 的成对出现——漏掉一个,就可能让几十个 goroutine 卡在读响应或等上下文取消上。










