Making concurrent API requests in Go
Tags: go, concurrency, api, microservices
Published: 2020-07-05 08:25:42

Making API calls from the backend is a pretty common scenario we all come across, especially when working with microservices. Sometimes we even have to make multiple calls at the same time and doing it sequentially will be inefficient. So in this article, let us see how to implement concurrency when making multiple API calls. 

### Encapsulating request code

Taking a look at what we have got we have 
- a struct called Comic which contains the fields required fields from the response
- a function to make the request to XKCD and to return the decoded response

```go
type Comic struct {
	Num   int    `json:"num"`
	Link  string `json:"link"`
	Img   string `json:"img"`
	Title string `json:"title"`
}

const baseXkcdURL = "https://xkcd.com/%d/info.0.json"

func getComic(comicID int) (comic *Comic, err error) {
	url := fmt.Sprintf(baseXkcdURL, comicID)
	response, err := http.Get(url)
	if err != nil {
		return nil, err
	}

	err = json.NewDecoder(response.Body).Decode(&comic)
	if err != nil {
		return nil, err
	}

	return comic, nil
}

```

### Getting started

Now coming to our basic version of the code down.
What it does is:

- takes an integer array of comic IDs and loops over it
- for each ID, retrieves the response from `getComic` and sets it in the map

As you can see, the code is pretty straightforward and should function effectively in most cases. But the problem comes with execution time.

```go
func main() {
	start := time.Now()
	defer func() {
		fmt.Println("Execution Time: ", time.Since(start))
	}()

	comicsNeeded := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
	comicMap := make(map[int]*Comic, len(comicsNeeded))

	for _, id := range comicsNeeded {
		comic, err := getComic(id)
		if err != nil {
      continue
		}
    comicMap[id] = comic
		fmt.Printf("Fetched comic %d with title %v\n", id, comic.Title)
	}
}
```

A glance at the output can tell us that the output delivered is sequential as expected, since the each API call is made only after the previous one is done.

So the execution took well above 7 seconds, which would not be ideal if the server was dealing with heavy load at scale. 


[![asciicast](https://asciinema.org/a/2Zr4KJc6LphYAd3YZrG6SefCp.svg)](https://asciinema.org/a/2Zr4KJc6LphYAd3YZrG6SefCp)

### Making it concurrent

Compared to other languages, I believe that Go has much better concurrency support. We will be using goroutines here, they are lightweight threads that run concurrently and can be spawned easily, thanks to Go.

After modifying the main function to spawn goroutines, here's how it looks like

```go
func main() {
	start := time.Now()
	defer func() {
		fmt.Println("Execution Time: ", time.Since(start))
	}()

	comicsNeeded := []int{11, 22, 33, 44, 55, 66, 77, 88, 99, 100}
	comicMap := make(map[int]*Comic)
  wg := sync.WaitGroup{}

	for _, id := range comicsNeeded {
		wg.Add(1)
		go func(id int) {
			comic, err := getComic(id)

			if err != nil {
				return
			}

			comicMap[id] = comic
			fmt.Printf("Fetched comic %d with title %v\n", id, comic.Title)
			wg.Done()
		}(id)
	}

	wg.Wait()
}
```

Okay, what's different? 

1. We are creating a WaitGroup first in  `wg := sync.WaitGroup`. The waitgroup can be thought of as a global counter which we can use to wait for all the goroutines to finish.
2. While we loop over the `comicNeeded` array, `wg.Add(1)` is used to indicate that we are creating a goroutine.
3. The `go func(){...}()` fires the anonymous function as a separate goroutine. The id argument is used to capture the loop variable at that point of time. 
4. Inside the goroutine, we wait for `getComic` so we can append it to the map. Once that is done `wg.Done()` this indicates that this goroutine has finished.
5. And finally after the loop is over, we call `wg.Wait()`. Why is this necessary?
When spawning of all the goroutines has been completed, the main thread has no work left to do, which will intuitively lead to its termination. However, it is undesirable, as it causes all the goroutines to quit. So in order to make the main thread wait till all the spawned goroutines are marked as done, we perform `wg.Wait()`.

Phew, now take a look at the output.

[![asciicast](https://asciinema.org/a/SVgghMjYOHRX0a4qkDwAi0UfW.svg)](https://asciinema.org/a/SVgghMjYOHRX0a4qkDwAi0UfW)

Woah, a reduction in execution time by a huge factor! The calls are not sequential as each goroutine executes independently based on the CPU availability. 

### Conclusion

To wrap it up concurrency, depending on the application, can give huge performance boosts to your code. I wanted to do this using Channels at first but later realised that it could be done much simpler using just WaitGroups. But I do have a blog next planned with the use of channels in the same context. 

### Notes

- [Concurrency should not be confused with parallelism.](https://www.youtube.com/watch?v=cN_DpYBzKso&t=212s)
- [Goroutines](https://gobyexample.com/goroutines)
- [Waitgroups](https://gobyexample.com/waitgroups)