Go语言如何改造getIPSetInfo函数实现HTTP请求并发执行
实现思路
你当前的getIPSetInfo为串行执行逻辑,循环内逐个发起HTTP请求、等待响应、解析结果,所有请求的IO等待时间线性累加,执行效率极低。Go的goroutine是轻量级执行单元,非常适合这类IO密集型并行场景,重构时只要规避并发写竞态、保证结果顺序、补上缺失的错误处理即可。
基础并发实现(无并发数限制)
这个版本会为每个IPSet ID启动一个goroutine发请求,通过sync.WaitGroup等待所有请求完成;预分配固定长度的结果切片,每个goroutine按传入的索引写入对应位置,既避免了并发写切片的竞态问题,也能保证返回结果的顺序和传入的ipsetIds顺序完全一致。
import ( "encoding/json" "fmt" "log" "os" "sync" "text/template" // 其他原有依赖包 ) // IpsetInfo、ObjDesc结构体保持原有定义不变即可 func getIPSetInfo(ipsetIds []string) ([]IpsetInfo, error) { // 预分配和入参长度一致的结果切片,避免并发append的竞态问题 result := make([]IpsetInfo, len(ipsetIds)) var wg sync.WaitGroup // 带缓冲的错误通道,收集各goroutine的运行错误 errChan := make(chan error, len(ipsetIds)) for i, id := range ipsetIds { wg.Add(1) // 索引、id作为参数传入goroutine,避免闭包变量捕获导致的数据错误 go func(idx int, ipsetId string) { defer wg.Done() url := "https://api-cloud-platform.com/cloudapi/1.0.0/firewallGroups/" + ipsetId respCode, body := httpGet(url, map[string]string{ "Authorization": "Bearer " + accessToken, }) // 校验请求状态,可根据你自己封装的httpGet返回逻辑调整 if respCode != 200 { errChan <- fmt.Errorf("request ipset %s failed, status code: %d", ipsetId, respCode) return } var item IpsetInfo if err := json.Unmarshal([]byte(body), &item); err != nil { errChan <- fmt.Errorf("parse ipset %s response failed: %w", ipsetId, err) return } // 按索引写入对应位置,无并发冲突 result[idx] = item }(i, id) } // 所有请求完成后关闭错误通道 wg.Wait() close(errChan) // 检查是否有请求失败 for err := range errChan { if err != nil { return nil, err } } return result, nil }
对应的main函数适配错误返回即可,建议用defer处理文件关闭,避免异常分支下的句柄泄漏:
func main() { ipsetIds := getIPSetIDs() IPSets, err := getIPSetInfo(ipsetIds) if err != nil { log.Fatalf("get ipset info failed: %v", err) } tmpl, err := template.ParseFiles("ipsets.template") if err != nil { log.Fatal(err) } f, err := os.Create("result.tf") if err != nil { log.Println("create file: ", err) return } defer f.Close() err = tmpl.Execute(f, IPSets) if err != nil { log.Print("execute: ", err) return } }
注:如果你的httpGet封装本身会返回error类型,可以直接替换上述代码里的状态码判断逻辑。
进阶:添加并发数限制
如果ipsetIds列表很长(比如上百个),无限制并发请求很可能触发云平台API限流,甚至耗尽本地端口资源。可以通过带缓冲的channel实现简单的并发闸门,比如限制最多同时发起10个请求,只需要修改getIPSetInfo的逻辑即可:
func getIPSetInfo(ipsetIds []string, maxConcurrent int) ([]IpsetInfo, error) { result := make([]IpsetInfo, len(ipsetIds)) var wg sync.WaitGroup errChan := make(chan error, len(ipsetIds)) // 缓冲区大小即为最大并发数 semaphore := make(chan struct{}, maxConcurrent) for i, id := range ipsetIds { wg.Add(1) go func(idx int, ipsetId string) { defer wg.Done() // 拿到令牌才发起请求,令牌耗尽时阻塞等待其他请求释放 semaphore <- struct{}{} defer func() { <-semaphore }() // 下方请求、解析逻辑和基础版本完全一致 url := "https://api-cloud-platform.com/cloudapi/1.0.0/firewallGroups/" + ipsetId respCode, body := httpGet(url, map[string]string{ "Authorization": "Bearer " + accessToken, }) if respCode != 200 { errChan <- fmt.Errorf("request ipset %s failed, status code: %d", ipsetId, respCode) return } var item IpsetInfo if err := json.Unmarshal([]byte(body), &item); err != nil { errChan <- fmt.Errorf("parse ipset %s response failed: %w", ipsetId, err) return } result[idx] = item }(i, id) } wg.Wait() close(errChan) close(semaphore) for err := range errChan { if err != nil { return nil, err } } return result, nil }
调用时指定并发数即可,例如IPSets, err := getIPSetInfo(ipsetIds, 10)即代表最多同时跑10个请求。
注意事项
- 禁止在goroutine内直接使用循环变量
i、id,必须作为参数传入goroutine,否则会因为闭包变量捕获,导致所有goroutine拿到最后一次循环的值,引发数据错误 - 请复用全局
http.Client实例,不要每个请求新建Client,标准库http.Client本身是并发安全的,复用可以减少连接开销 - 如果
accessToken会在运行时动态更新,需要加读写锁保护,避免并发读写出错
内容的提问来源于stack exchange,提问作者Vladimir Fomin
相关产品推荐
相关产品推荐

