I once wrote a quick tool to automate manual tasks at work. ,I had to make a bunch of API calls for environment A, take the response object and put them in a list. I had to do the same for another environment B and later compare both lists for different use cases. Due to time and my limited knowledge of using go concurrency primitives back then, I just wrote them in a blocking manner which worked fine. In this post I would like to revisit how i could write the same logic usig go concurrency primitives. One of the reasons why this use case can benefit from goroutines is that i don’t need the response in the order, i can store them in any order since i was planning to convert them into map for comparison.

for this post, I’m assuming that you read about goroutines and channels but not sure how to use them. Go through the code comments to understand and how I used them in my case.

Challenges:

  1. How to spin go routine and make sure the main go routine waits, because when you are new to go and testing about “go” keyword, you will quickly realize that main is also go routine and you should stop main goroutine until others are finishing other wise you go routine never executes. This is solved by waitGroup which we will see in the code.
  2. How to send data from goroutine to the main methods. I.e collect responses from each go routine. This is solved by Channels
  3. How to retrive the values from channel and use them - Range helps solve this issue

Please go through code comments to understand the through process and usage

package main


import (
   "fmt"
   "math/rand"
   "sync"
   "time"
)


// MockAPICall simulates an API call by returning mock data with a random wait time.
func MockAPICall(url string) (string, error) {


   // let's simulate api respones delay using rand.Intn and time.Sleep
   minDelay := 500  // Minimum delay in milliseconds
   maxDelay := 2000 // Maximum delay in milliseconds
   delay := rand.Intn(maxDelay-minDelay+1) + minDelay
   time.Sleep(time.Duration(delay) * time.Millisecond)

   // since Error is a string in go , uncomment to see how error is sent in the channel.
   // if url == "https://myapi.example.com/user/2" {
   //  return "", fmt.Errorf("got an error")
   // }
   // Mock response
   return "MyMockResponse" + url, nil
}

// Actual go routine  that condinates the api call.
func makeAPICall(url string, wg *sync.WaitGroup, results chan string) {
   // defer keyword is the last time to run in the method , sinaling the methods work is done at the end
   defer wg.Done()

   response, err := MockAPICall(url)
   if err != nil {
       results <- fmt.Sprintf("Error: %v", err)
       return
   }
   //We send the reponse to the channel using <- . Once the response is sent methods runs the defer wg.Done()
   results <- response
}


func main() {
   // Assuming mulitple URL we have that need to be called.
   urls := []string{
       "https://myapi.example.com/user/1",
       "https://myapi.example.com/user/2",
       "https://myapi.example.com/user/3",
       "https://myapi.example.com/user/4",
       "https://myapi.example.com/user/5",
       "https://myapi.example.com/user/6",
       "https://myapi.example.com/user/7",
       "https://myapi.example.com/user/8",
       "https://myapi.example.com/user/9",
       "https://myapi.example.com/user/10",
       "https://myapi.example.com/user/11",
   }
   //defines a waitGroup, waitGroup help wait for multiple goroutine to finish. usally used in the main method so that main can waiting untill all the other
   // goroutine can finish otherwise main will compelte and there by ending the program.
   var wg sync.WaitGroup

   //we are defining a buffered channel using make, buffered channel accepts vaule that are defined while creating. see here https://gobyexample.com/channel-buffering
   // Note that you can send anything in the channel as longs as type is statisfied. You can see that we have option to send error in the makeAPICall method.
   results := make(chan string, len(urls))

   for _, url := range urls {
       //increments waitgroup count by 1 and in next line creates a go routine.
       wg.Add(1)
       //using go keywords to start a goroutine. this will spin go rountine in each time for loops.
       go makeAPICall(url, &wg, results)
   }
   //above we are incrementing wait group for each new goroutine, once the goroutine does it work in makeAPICall method we are calling wg.done which will
   //decretement the waitGroup count. in this fashion when all go routines are done with their work, the waitGroup counter will be zero. Now, untill the counter
   //become zero, below main methods will wait at below line wg.wait(). that is the purpose of wait() in waitGrou.
   wg.Wait()

   // at this point , all go routine are done with thier work since we already called wg.Wait(). So we should close channel. If you try to comment this line and
   //execute you will see that code will results in deadlock becuase the for loop in the next line will keep waiting forever since it never recevied close on the channe.
   //hence it is imporant to close the results channel to signal that we've collected all responses
   close(results)

   // for with range allows you to loop over channel as and when the value are sent. So here eveytime a goroutine send's the respone to channel, range will print it for us.
   // notw that if you don't close the channel in the abvoe , this loop will forever wait and result in a panic.
   for result := range results {
       fmt.Println(result)
   }
}