Go语言天生支持并发特性,通过goroutine和channel可以轻松实现高效的并发任务处理,批量获取URL列表就是非常典型的应用场景。相比串行逐个请求URL的方式,并发请求能大幅缩短整体执行时间,提升程序运行效率。

基础并发实现思路
核心思路是为每个URL启动一个goroutine发起请求,通过channel收集所有请求的结果,同时需要注意控制并发数量,避免同时发起过多请求导致系统资源耗尽或者触发目标服务的限流策略。
简单并发请求示例
下面是一个最基础的并发获取URL列表的实现,每个URL对应一个goroutine,请求完成后将结果发送到统一的channel中:
package main
import (
"fmt"
"io"
"net/http"
"sync"
"time"
)
// 定义请求结果结构体
type RequestResult struct {
URL string
StatusCode int
Body string
Err error
}
func fetchURL(url string, wg *sync.WaitGroup, resultChan chan<- RequestResult) {
// 函数执行完成后通知WaitGroup
defer wg.Done()
// 创建HTTP客户端,设置超时时间
client := &http.Client{
Timeout: 10 * time.Second,
}
// 发起GET请求
resp, err := client.Get(url)
if err != nil {
resultChan <- RequestResult{URL: url, Err: err}
return
}
defer resp.Body.Close()
// 读取响应内容
body, err := io.ReadAll(resp.Body)
if err != nil {
resultChan <- RequestResult{URL: url, Err: err}
return
}
// 发送成功结果到channel
resultChan <- RequestResult{
URL: url,
StatusCode: resp.StatusCode,
Body: string(body),
}
}
func main() {
// 待获取的URL列表
urlList := []string{
"http://ipipp.com",
"http://127.0.0.1:8080",
"http://192.168.0.1",
}
// 创建结果收集channel
resultChan := make(chan RequestResult, len(urlList))
var wg sync.WaitGroup
// 为每个URL启动goroutine
for _, url := range urlList {
wg.Add(1)
go fetchURL(url, &wg, resultChan)
}
// 等待所有goroutine执行完成,然后关闭channel
go func() {
wg.Wait()
close(resultChan)
}()
// 遍历channel获取所有结果
for result := range resultChan {
if result.Err != nil {
fmt.Printf("URL: %s 请求失败,错误: %vn", result.URL, result.Err)
} else {
fmt.Printf("URL: %s 状态码: %d 响应长度: %dn", result.URL, result.StatusCode, len(result.Body))
}
}
}
控制并发数量
上面的实现会为每个URL都启动一个goroutine,如果URL列表很长,比如有上千个URL,同时发起上千个HTTP请求会导致资源占用过高,甚至程序崩溃。这时候需要使用带缓冲的channel作为信号量来控制并发数量。
带并发限制的示例
下面的代码通过信号量channel限制同时最多只有5个goroutine在发起请求:
package main
import (
"fmt"
"io"
"net/http"
"sync"
"time"
)
type RequestResult struct {
URL string
StatusCode int
Body string
Err error
}
func fetchURL(url string, semaphore chan struct{}, wg *sync.WaitGroup, resultChan chan<- RequestResult) {
defer wg.Done()
// 获取信号量,控制并发数量
semaphore <- struct{}{}
defer func() { <-semaphore }()
client := &http.Client{
Timeout: 10 * time.Second,
}
resp, err := client.Get(url)
if err != nil {
resultChan <- RequestResult{URL: url, Err: err}
return
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
resultChan <- RequestResult{URL: url, Err: err}
return
}
resultChan <- RequestResult{
URL: url,
StatusCode: resp.StatusCode,
Body: string(body),
}
}
func main() {
urlList := []string{
"http://ipipp.com",
"http://ipipp.com/about",
"http://127.0.0.1:8080",
"http://192.168.0.1",
"http://ipipp.com/contact",
"http://ipipp.com/help",
"http://127.0.0.1:8081",
}
// 定义最大并发数为5
maxConcurrent := 5
semaphore := make(chan struct{}, maxConcurrent)
resultChan := make(chan RequestResult, len(urlList))
var wg sync.WaitGroup
for _, url := range urlList {
wg.Add(1)
go fetchURL(url, semaphore, &wg, resultChan)
}
go func() {
wg.Wait()
close(resultChan)
}()
for result := range resultChan {
if result.Err != nil {
fmt.Printf("URL: %s 请求失败,错误: %vn", result.URL, result.Err)
} else {
fmt.Printf("URL: %s 状态码: %d 响应长度: %dn", result.URL, result.StatusCode, len(result.Body))
}
}
}
常见问题处理
超时控制
在上面的示例中已经通过http.Client的Timeout字段设置了10秒的超时时间,这是避免单个请求卡住导致整个程序阻塞的重要手段。如果需要对单个请求设置更灵活的超时,也可以使用context包来实现。
错误处理
并发请求中部分URL请求失败是常见情况,需要在结果处理时区分成功和失败的请求,避免单个请求失败影响其他请求的结果收集。上面的示例中已经通过RequestResult结构体的Err字段记录了错误信息,方便后续统一处理。
结果顺序问题
并发请求的结果返回顺序和URL列表的顺序不一致是正常现象,因为不同URL的请求耗时不同。如果需要保持结果顺序和URL列表顺序一致,可以在发起请求时给每个任务添加序号,结果中携带序号,最后按照序号排序即可。