在Golang开发中,当需要同时向多个不同的接口发起HTTP请求时,串行执行的方式会大幅增加整体耗时,因此优化HTTP客户端的并发请求能力是提升程序性能的重要方向。合理的并发优化不仅能缩短请求总耗时,还能避免资源耗尽等问题。
基础并发请求实现
最基础的并发请求方式是直接为每个请求启动一个goroutine,配合sync.WaitGroup等待所有请求完成。以下是一个简单的示例:
package main
import (
"fmt"
"io/ioutil"
"net/http"
"sync"
"time"
)
func main() {
// 待请求的URL列表
urls := []string{
"http://127.0.0.1:8080/api1",
"http://127.0.0.1:8080/api2",
"http://127.0.0.1:8080/api3",
}
var wg sync.WaitGroup
// 记录开始时间
start := time.Now()
for _, url := range urls {
wg.Add(1)
// 启动goroutine执行请求
go func(u string) {
defer wg.Done()
resp, err := http.Get(u)
if err != nil {
fmt.Printf("请求 %s 失败: %vn", u, err)
return
}
defer resp.Body.Close()
body, _ := ioutil.ReadAll(resp.Body)
fmt.Printf("请求 %s 返回长度: %dn", u, len(body))
}(url)
}
// 等待所有请求完成
wg.Wait()
fmt.Printf("总耗时: %vn", time.Since(start))
}
优化方向一:复用HTTP客户端
默认的http.Get方法每次调用都会创建新的HTTP客户端,而HTTP客户端的传输层Transport支持连接复用,频繁创建客户端会浪费资源。我们可以提前初始化一个全局复用的HTTP客户端:
package main
import (
"fmt"
"io/ioutil"
"net/http"
"sync"
"time"
)
// 初始化可复用的HTTP客户端
var httpClient = &http.Client{
Transport: &http.Transport{
MaxIdleConns: 100, // 最大空闲连接数
MaxIdleConnsPerHost: 10, // 每个主机最大空闲连接数
IdleConnTimeout: 30 * time.Second, // 空闲连接超时时间
},
Timeout: 10 * time.Second, // 请求超时时间
}
func main() {
urls := []string{
"http://127.0.0.1:8080/api1",
"http://127.0.0.1:8080/api2",
"http://127.0.0.1:8080/api3",
}
var wg sync.WaitGroup
start := time.Now()
for _, url := range urls {
wg.Add(1)
go func(u string) {
defer wg.Done()
// 使用复用的客户端发起请求
resp, err := httpClient.Get(u)
if err != nil {
fmt.Printf("请求 %s 失败: %vn", u, err)
return
}
defer resp.Body.Close()
body, _ := ioutil.ReadAll(resp.Body)
fmt.Printf("请求 %s 返回长度: %dn", u, len(body))
}(url)
}
wg.Wait()
fmt.Printf("总耗时: %vn", time.Since(start))
}
优化方向二:控制并发数量
无限制启动goroutine可能会导致系统资源耗尽,尤其是请求数量较多时。我们可以通过带缓冲的channel实现并发数量控制,避免同时发起过多请求:
package main
import (
"fmt"
"io/ioutil"
"net/http"
"sync"
"time"
)
var httpClient = &http.Client{
Transport: &http.Transport{
MaxIdleConns: 100,
MaxIdleConnsPerHost: 10,
IdleConnTimeout: 30 * time.Second,
},
Timeout: 10 * time.Second,
}
func main() {
urls := []string{
"http://127.0.0.1:8080/api1",
"http://127.0.0.1:8080/api2",
"http://127.0.0.1:8080/api3",
"http://127.0.0.1:8080/api4",
"http://127.0.0.1:8080/api5",
}
// 控制最大并发数为3
concurrency := 3
semaphore := make(chan struct{}, concurrency)
var wg sync.WaitGroup
start := time.Now()
for _, url := range urls {
wg.Add(1)
go func(u string) {
defer wg.Done()
// 获取信号量,控制并发
semaphore <- struct{}{}
defer func() { <-semaphore }()
resp, err := httpClient.Get(u)
if err != nil {
fmt.Printf("请求 %s 失败: %vn", u, err)
return
}
defer resp.Body.Close()
body, _ := ioutil.ReadAll(resp.Body)
fmt.Printf("请求 %s 返回长度: %dn", u, len(body))
}(url)
}
wg.Wait()
fmt.Printf("总耗时: %vn", time.Since(start))
}
优化方向三:使用协程池减少goroutine创建开销
当请求数量非常多时,频繁创建和销毁goroutine也会带来额外开销,此时可以使用协程池统一管理goroutine。以下是一个简单的协程池实现示例:
package main
import (
"fmt"
"io/ioutil"
"net/http"
"sync"
"time"
)
var httpClient = &http.Client{
Transport: &http.Transport{
MaxIdleConns: 100,
MaxIdleConnsPerHost: 10,
IdleConnTimeout: 30 * time.Second,
},
Timeout: 10 * time.Second,
}
// 简单协程池结构
type GoroutinePool struct {
taskChan chan func() // 任务通道
wg sync.WaitGroup
}
// 初始化协程池
func NewGoroutinePool(size int) *GoroutinePool {
pool := &GoroutinePool{
taskChan: make(chan func(), 100),
}
// 启动固定数量的worker
for i := 0; i < size; i++ {
go pool.worker()
}
return pool
}
// worker执行任务
func (p *GoroutinePool) worker() {
for task := range p.taskChan {
task()
}
}
// 提交任务到协程池
func (p *GoroutinePool) Submit(task func()) {
p.wg.Add(1)
p.taskChan <- func() {
defer p.wg.Done()
task()
}
}
// 等待所有任务完成
func (p *GoroutinePool) Wait() {
p.wg.Wait()
close(p.taskChan)
}
func main() {
urls := []string{
"http://127.0.0.1:8080/api1",
"http://127.0.0.1:8080/api2",
"http://127.0.0.1:8080/api3",
"http://127.0.0.1:8080/api4",
"http://127.0.0.1:8080/api5",
}
// 创建大小为3的协程池
pool := NewGoroutinePool(3)
start := time.Now()
for _, url := range urls {
u := url
pool.Submit(func() {
resp, err := httpClient.Get(u)
if err != nil {
fmt.Printf("请求 %s 失败: %vn", u, err)
return
}
defer resp.Body.Close()
body, _ := ioutil.ReadAll(resp.Body)
fmt.Printf("请求 %s 返回长度: %dn", u, len(body))
})
}
pool.Wait()
fmt.Printf("总耗时: %vn", time.Since(start))
}
优化注意事项
- 避免在goroutine中直接使用循环变量的引用,需要将循环变量作为参数传入goroutine,防止变量值被覆盖。
- 合理设置HTTP客户端的超时时间,避免出现请求长时间阻塞的情况。
- 根据实际的请求场景调整并发数量,过高的并发可能会触发目标服务的限流策略。
- 及时关闭响应体
resp.Body,避免资源泄漏。
以上优化方法可以根据实际业务场景组合使用,在提升请求效率的同时保证程序的稳定性。
GolangHTTP_clientconcurrent_requestgoroutine_pool修改时间:2026-06-27 17:39:45