Go语言是一种非常适合并发编程的编程语言,在实现高并发的服务或应用时,它的效能得到了很好的发挥。在日常开发中,我们可能会遇到需要并发请求接口或者并发处理大量的数据的场景,本文将介绍golang中如何实现并发请求接口。

并发请求接口的场景

在实际开发中,我们可能会遇到需要请求某个接口并获取响应数据的场景,比如:

  • 获取某个网站上的商品数据。
  • 从不同的API接口获取数据并汇总呈现。
  • 同时请求多个数据源以便快速收集数据。

在单线程中,如果需要请求多个接口,那么需要一个接口请求完成之后再去请求另一个接口,这会导致整个流程变得缓慢。相反,使用并发请求接口则可以同时发起多个请求,大大提高请求效率。

goroutine并发处理

goroutine是go语言中的一种特殊的函数,它可以在与主线程并行的特殊线程中运行。多个goroutine同时运行可以同时请求多个接口,请求完成之后再进行数据整合处理。并发使用goroutine比较容易实现,通过go关键字实现即可。

WaitGroup控制goroutine

在实际开发中,我们可能会发现可能有一些协程可能比较耗时,可能需要更多的时间才能返回结果。在这种情况下,我们需要等待协程返回结果,进行后面的处理。这时候,我们需要使用sync.WaitGroup来控制goroutine数量,确保所有请求都得到响应结果。

package main

import (
    "fmt"
    "io/ioutil"
    "net/http"
    "sync"
)

var wg sync.WaitGroup // 声明一个sync.WaitGroup实例,用于协程控制

func main() {
    urls := []string{"https://www.baidu.com", "https://www.qq.com", "https://www.taobao.com", "https://www.jd.com", "https://www.mi.com"}

    // 通过遍历urls,启动goroutine
    for _, url := range urls {
        wg.Add(1) // 添加一个goroutine
        go getBody(url)
    }

    wg.Wait() // 等待所有goroutine结束
}

// getBody用于获取传入url的响应结果,并打印。
func getBody(url string) {
    resp, err := http.Get(url) // 发起http GET请求
    if err != nil {
        fmt.Println(err)
        return
    }
    defer resp.Body.Close()
    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        fmt.Println(err)
        return
    }
    fmt.Printf("url: %s, contents:
%s
", url, string(body))
    wg.Done() // 相当于wg.Add(-1),标志该goroutine已经结束
}
main()go getBody(url)wg.Done()
wg.Wait()

并发请求的最佳实践

在实际开发中,我们需要注意一些细节,这些细节可以帮助我们更好地使用并发请求接口。

一、并发数量的控制

在并发请求接口的时候,我们需要控制并发的数量,特别是当接口请求数量比较大时,避免一次性请求使服务器受到太大压力。我们可以设立一个最大值,这样可以保证并发的最高数量。我们可以使用golang中的缓冲通道实现最大并发数的控制。

ch := make(chan struct{}, 5) // 声明一个缓冲通道,大小为5,控制并发数量为5

for _, url := range urls {
    ch <- struct{}{} // 把协程数量放在通道里
    wg.Add(1)  // 添加一个goroutine
    go func(url string) {
        defer wg.Done()
        getBody(url)
        <-ch // 从通道里取出一个值,表示这个协程已经结束
    }(url)
}

在声明缓冲通道的过程中,我们设置缓冲大小为5,表示最多同时运行5个goroutine,接着我们遍历urls,向通道中加入结构体值。

func(url string)getBody(url)<-ch

二、避免请求阻塞

在进行并发请求接口的时候,我们需要避免请求阻塞,通常出现在在一个请求长时间没有相应时。我们可以使用Golang中的context.Context解决这个问题。如果请求超时,则取消阻塞的请求。

url := "https://httpstat.us/200?sleep=8000"

ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*5000) // 告诉请求,5秒之后自动取消

defer cancel()

req, err := http.NewRequestWithContext(ctx, "GET", url, nil) // 使用请求上下文

if err != nil {
    log.Fatal(err)
}

client := http.DefaultClient
resp, err := client.Do(req) // 发起请求
if err != nil {
    log.Fatal(err)
}

if resp.StatusCode == http.StatusOK {
    contents, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("%s
", contents)
}
context.WithTimeouthttp.DefaultClient

当请求超时时,请求链路就会被直接关掉。这时我们会受到“context deadline exceeded”错误的提示。

三、避免请求重复

在请求接口时,可能会遇到重复请求同一个接口的情况,在这种情况下,我们应该避免重复请求同一个接口,这会浪费宝贵的时间和资源。我们可以使用Golang中的sync.Map解决这个问题。

var m = sync.Map{}

url := "https://httpbin.org/get"

wg.Add(2)
go doGet(url, &m, &wg)
go doGet(url, &m, &wg)

wg.Wait()

func doGet(url string, m *sync.Map, wg *sync.WaitGroup) {
    _, loaded := m.LoadOrStore(url, true) // 表示url已经被请求过,如果已存在,则直接返回,否则返回false并储存

    if loaded {
        fmt.Printf("url %s already requested.
", url)
        wg.Done()
        return
    }

    resp, err := http.Get(url)
    if err != nil {
        log.Fatal(err)
    }

    defer resp.Body.Close()
    contents, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("%s
", contents)
    wg.Done()
}
doGetreturnwg.Done()

总结

本文介绍了使用golang实现并发请求接口的方法。通过使用goroutine并发处理,WaitGroup协程控制,以及缓冲通道控制并发数量。通过在请求上下文中设置超时来避免请求阻塞,并且使用sync.Map避免请求重复。通过使用这些技术,我们可以大大提高请求接口的效率,提升编码效率和编程体验。