Concurrency in Go (Part 3)

The most common use case is when developers are trying to send multiple HTTP requests to different endpoints. Let’s say we want to send to 10 endpoints. The average time to execute each endpoint is 100ms. We will get 10 endpoints x 100ms = 1000ms if we do it sequentially. This is a raw calculation. It does not include how we parse the response yet. Let’s solve this case properly.

First, let's reproduce the problem.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
package main

import (
  "encoding/json"
  "errors"
  "fmt"
  "io"
  "log"
  "net/http"
  "time"

  "golang.org/x/sync/errgroup"
)

var (
  errProducts = errors.New("failed to retrieve products")
)

type product struct {
  ID    uint    `json:"id"`
  Title string  `json:"title"`
  Price float64 `json:"price"`
}

type ProductRepository struct {
  Client *http.Client
}

func (p ProductRepository) Products() ([]product, error) {
  products := []product{}
  for i := 1; i <= 10; i++ {
    resp, err := p.Client.Get(fmt.Sprintf("https://dummyjson.com/products/%d", i))
    if err != nil {
      log.Println(err)

      return nil, errProducts
    }

    body, err := io.ReadAll(resp.Body)
    if err != nil {
      log.Println(err)

      return nil, errProducts
    }

    product := product{}
    if err := json.Unmarshal(body, &product); err != nil {
      log.Println(err)

      return nil, errProducts
    }

    products = append(products, product)
  }

  return products, nil
}

func main() {
  productService := ProductRepository{
    Client: http.DefaultClient,
  }

  start := time.Now()
  products, err := productService.Products()
  if err != nil {
    log.Panic(err)
  }

  log.Println(products)
  log.Println(time.Since(start))
}

It looks like our code is clean. That code was executed for around 3.4s. Take a look at the code carefully. What is the bug?

Okay, here is a better version.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
func (p ProductRepository) Products() ([]product, error) {
  n := 10
  g, products := new(errgroup.Group), make([]product, n)
  for i := 1; i <= n; i++ {
    g.Go(func() error {
      resp, err := p.Client.Get(fmt.Sprintf("https://dummyjson.com/products/%d", i))
      if err != nil {
        log.Println(err)

        return errProducts
      }

      body, err := io.ReadAll(resp.Body)
      if err != nil {
        log.Println(err)

        return errProducts
      }

      product := product{}
      if err := json.Unmarshal(body, &product); err != nil {
        log.Println(err)

        return errProducts
      }

      products[i-1] = product

      return nil
    })
  }
  if err := g.Wait(); err != nil {
    return nil, err
  }
  return products, nil
}

That code successfully reduces the execution time from around 3.4s to around 840ms. Here are the points:

The code above is an example of using errgroup.Group. You can use the same technique for gRPC, GraphQL, etc.

Related Articles