必须复用http.Client实例,因其Transport连接池不共享,否则引发TIME_WAIT激增、DNS重复解析和TLS握手开销;应全局复用并显式配置MaxIdleConns、MaxIdleConnsPerHost、IdleConnTimeout,用context.WithTimeout控制单请求超时,通过带缓冲channel(如sem := make(chan struct{}, 10))限制并发数防打爆fd或触发限流。

HTTP客户端复用 http.Client 是必须的
默认每次新建 http.Client 会创建独立的 http.Transport,而 Transport 内部的连接池(IdleConnTimeout、MaxIdleConns 等)不会共享。不复用会导致大量 TIME_WAIT 连接、DNS 重复解析、TLS 握手开销激增。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 全局复用一个
http.Client实例,不要在函数内 new - 显式配置
Transport:设置MaxIdleConns(如 100)、MaxIdleConnsPerHost(如 100)、IdleConnTimeout(如 30 * time.Second) - 若需超时控制,优先用
context.WithTimeout传入Do,而非设置Client.Timeout(它会覆盖所有请求,不够灵活)
避免阻塞式并发:用 goroutine + channel 控制并发数
直接启动成百上千 goroutine 发请求,容易打爆本地文件描述符(fd)、远端服务限流或触发 TCP 拥塞。Go 的调度器不负责 HTTP 请求排队,需手动节流。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 用带缓冲的 channel 做信号量,限制最大并发数(如
sem := make(chan struct{}, 10)) - 每个请求前
sem ,完成后 - 不用
sync.WaitGroup单纯等 goroutine 结束——它不控并发,只管同步 - 慎用
runtime.GOMAXPROCS调整,HTTP 并发瓶颈通常不在 CPU,而在网络 I/O 和连接池
响应体必须显式关闭 resp.Body
漏掉 defer resp.Body.Close() 会导致底层 TCP 连接无法归还给连接池,MaxIdleConns 形同虚设,后续请求被迫新建连接,性能断崖下跌。
常见错误现象:
- 程序运行一段时间后 QPS 持续下降,
netstat -an | grep :443 | wc -l显示 ESTABLISHED 数量持续上涨 - 抓包发现大量重复 TLS 握手
正确写法示例:
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close() // 必须放在这里,不能只在 success 分支
body, _ := io.ReadAll(resp.Body) // 或用 streaming 处理大响应
小响应体别用 io.ReadAll,大响应体别全读进内存
io.ReadAll 会把整个响应体拷贝到内存,对大文件(如图片、CSV)极易 OOM;对极小响应(如 JSON {"ok":true}),又因分配 slice+拷贝带来不必要开销。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 已知响应小且结构固定,用
json.NewDecoder(resp.Body).Decode(&v)流式解码,零拷贝 - 响应大或需校验完整性,用
io.Copy(io.Discard, resp.Body)忽略内容但释放连接 - 需要部分读取(如取 header 或前几 KB),用
io.LimitReader(resp.Body, 8192)配合bufio.NewReader
连接复用和资源释放的细节,比 goroutine 数量更影响真实并发吞吐——多数人调高 GOMAXPROCS 或加 goroutine,却忘了 Body.Close() 漏了一行。











