在Golang的网络编程场景中,当需要同时调用多个第三方HTTP接口或者向多个服务节点发起请求时,串行执行请求会导致整体耗时等于所有请求耗时之和,效率极低。通过并发的方式处理HTTP客户端请求,可以大幅缩短整体等待时间,提升程序的响应速度。

基础并发方案:goroutine + channel
goroutine是Golang原生的并发执行单元,配合channel可以实现请求结果的传递和同步。我们可以为每个HTTP请求启动一个goroutine,将请求结果发送到channel中,主协程统一收集结果。
首先定义请求结果的通用结构体,方便统一处理不同请求的结果:
package main
import (
"fmt"
"io/ioutil"
"net/http"
"time"
)
// 请求结果结构体
type RequestResult struct {
URL string // 请求的URL
Body string // 响应内容
Err error // 错误信息
Cost time.Duration // 请求耗时
}
接下来实现并发请求的逻辑,为每个URL启动一个goroutine发起请求,结果发送到channel:
func main() {
// 待请求的URL列表
urls := []string{
"http://127.0.0.1:8080/api1",
"http://127.0.0.1:8080/api2",
"http://192.168.0.1:8080/api3",
}
resultChan := make(chan RequestResult, len(urls))
startTime := time.Now()
// 为每个URL启动一个goroutine发起请求
for _, url := range urls {
go func(targetURL string) {
reqStart := time.Now()
resp, err := http.Get(targetURL)
if err != nil {
resultChan <- RequestResult{
URL: targetURL,
Err: err,
Cost: time.Since(reqStart),
}
return
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
resultChan <- RequestResult{
URL: targetURL,
Err: err,
Cost: time.Since(reqStart),
}
return
}
resultChan <- RequestResult{
URL: targetURL,
Body: string(body),
Cost: time.Since(reqStart),
}
}(url)
}
// 收集所有结果
results := make([]RequestResult, 0, len(urls))
for i := 0; i < len(urls); i++ {
result := <-resultChan
results = append(results, result)
}
totalCost := time.Since(startTime)
fmt.Printf("所有请求完成,总耗时: %vn", totalCost)
for _, res := range results {
if res.Err != nil {
fmt.Printf("URL: %s, 请求失败: %v, 耗时: %vn", res.URL, res.Err, res.Cost)
} else {
fmt.Printf("URL: %s, 响应长度: %d, 耗时: %vn", res.URL, len(res.Body), res.Cost)
}
}
}
带并发控制的方案:sync.WaitGroup
上面的方案没有限制并发数量,如果URL数量过多,会同时启动大量goroutine,可能导致系统资源耗尽。使用sync.WaitGroup可以等待所有请求完成,同时可以结合缓冲channel实现并发数量控制。
下面的示例实现了限制最大并发数为3的HTTP请求逻辑:
package main
import (
"fmt"
"io/ioutil"
"net/http"
"sync"
"time"
)
type RequestResult struct {
URL string
Body string
Err error
}
func main() {
urls := []string{
"http://ipipp.com/api1",
"http://ipipp.com/api2",
"http://ipipp.com/api3",
"http://ipipp.com/api4",
"http://ipipp.com/api5",
}
maxWorker := 3 // 最大并发数
urlChan := make(chan string, len(urls))
resultChan := make(chan RequestResult, len(urls))
// 将URL放入channel
for _, url := range urls {
urlChan <- url
}
close(urlChan)
// 启动工作协程
var wg sync.WaitGroup
for i := 0; i < maxWorker; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for url := range urlChan {
resp, err := http.Get(url)
if err != nil {
resultChan <- RequestResult{URL: url, Err: err}
continue
}
body, err := ioutil.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
resultChan <- RequestResult{URL: url, Err: err}
continue
}
resultChan <- RequestResult{URL: url, Body: string(body)}
}
}()
}
// 等待所有工作协程完成,关闭结果channel
go func() {
wg.Wait()
close(resultChan)
}()
// 收集结果
for res := range resultChan {
if res.Err != nil {
fmt.Printf("URL: %s, 请求失败: %vn", res.URL, res.Err)
} else {
fmt.Printf("URL: %s, 响应长度: %dn", res.URL, len(res.Body))
}
}
}
错误处理与超时控制
实际场景中,HTTP请求可能会出现超时、连接失败等问题,需要为请求添加超时控制,同时合理处理错误,避免单个请求失败影响整体流程。
可以使用http.Client的Timeout字段设置全局超时,也可以在单个请求中使用context设置更灵活的超时时间:
func requestWithTimeout(url string, timeout time.Duration) RequestResult {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return RequestResult{URL: url, Err: err}
}
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return RequestResult{URL: url, Err: err}
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return RequestResult{URL: url, Err: err}
}
return RequestResult{URL: url, Body: string(body)}
}
两种方案对比
两种常用方案的特点对比如下:
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| goroutine + channel | 实现简单,无需额外同步逻辑 | 无并发控制,URL过多时goroutine数量不可控 | 请求数量少,无需限制并发的场景 |
| sync.WaitGroup + 并发控制 | 可控制并发数量,资源占用可控 | 实现逻辑稍复杂,需要管理channel和WaitGroup | 请求数量多,需要限制并发的场景 |
注意事项
- goroutine中使用的变量如果是循环变量,需要通过参数传入,避免闭包捕获变量的问题,比如前面示例中把url作为参数传入goroutine函数。
- HTTP响应体必须调用
Close()方法关闭,否则会造成资源泄漏,建议使用defer处理。 - channel的缓冲大小可以根据实际需求设置,合理设置缓冲可以减少goroutine阻塞的情况。
- 如果请求需要携带自定义Header、请求体等,可以在创建请求时通过
http.NewRequest方法实现,再使用client.Do发起请求。
GolangHTTP_clientgoroutinechannelsync_WaitGroup修改时间:2026-07-04 18:12:14