Asynchronous Programming with Go
When I started learning Go, a couple of its features had really drawn me towards it. The first was the ease in which pointers could be used. The second was the ease with which its asynchronous capabilities could be used. In this blog, I would try to use the different ways you can solve a very simple problem.
Let us take a very common example where we would simply try to build a program which would check if a set of websites/urls are up and running or not. For this example, I have picked 5 airline websites.
1. The Synchronous way#
The most easiest way to handle this would be to simple iterate over your slice of urls and try to make an HTTP GET request to the url. Consider the following code snippet:
package main
import (
"fmt"
"net/http"
)
func checkUrl(url string) {
_, err := http.Get(url)
if err != nil {
fmt.Println(url, "is down !")
return
}
fmt.Println(url, "is up and running.")
}
func main() {
urls := []string{
"https://www.easyjet.com/",
"https://www.skyscanner.de/",
"https://www.ryanair.com",
"https://wizzair.com/",
"https://www.swiss.com/",
}
for _, url := range urls {
checkUrl(url)
}
}
The code snippet is fairly basic and when I run it, I get the following output:
https://www.easyjet.com/ is up and running.
https://www.skyscanner.de/ is up and running.
https://www.ryanair.com is up and running.
https://wizzair.com/ is up and running.
https://www.swiss.com/ is up and running.
This is good, but in this case we check if each website is up one by one. An ideal solution would be to handle this non-linearly and check if they are up concurrently.
2. Go routines#
Go ships with go routines, which executes a function asynchronously. It is a lightweight thread of execution. The keyword for it is go. Lets say, you have a function f() and you would like to execute that in a go routine, then simply use go f(). So in the above example, all we need to do is simply add the keyword go before the function call for checkUrl, and the function would be executed asynchronously in a go routine.
func main() {
urls := []string{
"https://www.easyjet.com/",
"https://www.skyscanner.de/",
"https://www.ryanair.com",
"https://wizzair.com/",
"https://www.swiss.com/",
}
for _, url := range urls {
go checkUrl(url)
}
}
Just the line with go checkUrl(url) is different from the previous example. Now, if we execute this code snippet, we will notice that it ends immediately and does not print anything. Let me explain why that happened.
The go routines are executed within the main() routine. What we can see is the execution of each of the go routines takes longer than the execution of the main() routine, i.e., the main routine exits before all the go routines have finished execution.
Now how can we not allow the main routine to not exit so soon? One simple solution would be to add a sleep function at the end:
import "time"
func main() {
// ... urls slice ...
for _, url := range urls {
go checkUrl(url)
}
time.Sleep(5 * time.Second)
}
Now if we execute the code, we get the following output:
https://www.ryanair.com is up and running.
https://www.swiss.com/ is up and running.
https://www.skyscanner.de/ is up and running.
https://wizzair.com/ is up and running.
https://www.easyjet.com/ is up and running.
Notice that the order of the output is different from that of our urls slice. This is because they have been executed asynchronously. However, having a sleep function is not a good solution.
In the next section, we will talk about a primitive way to handle this with Wait Groups.
3. Wait Groups#
The sync package of Go provides some basic synchronization primitives such as mutexes, WaitGroups, etc. In this example, we would be using the WaitGroup.
A WaitGroup waits for a collection of go routines to finish. Just simply add the number of go routines to wait for in the main routine. Let us use it in our example:
package main
import (
"fmt"
"net/http"
"sync"
)
func checkUrl(url string, wg *sync.WaitGroup) {
defer wg.Done()
_, err := http.Get(url)
if err != nil {
fmt.Println(url, "is down !")
return
}
fmt.Println(url, "is up and running.")
}
func main() {
urls := []string{
"https://www.easyjet.com/",
"https://www.skyscanner.de/",
"https://www.ryanair.com",
"https://wizzair.com/",
"https://www.swiss.com/",
}
var wg sync.WaitGroup
for _, url := range urls {
wg.Add(1)
go checkUrl(url, &wg)
}
wg.Wait()
}
We defined a WaitGroup instance. We increment with 1 just before we execute each go routine. We have a deferred call to the Done() method of WaitGroup — this will be a signal to the WaitGroup to let it know that the execution of the go routine is complete. Finally, we added the Wait() method call. This will let the main routine simply wait until all the Done() calls of the WaitGroup have been executed.
WaitGroup generally should be used for much lower level designs. For better communication, we should use channels.
4. Channels#
One issue with the solution above was that there was no communication from the function checkUrl back to the main routine. This is essential for better communication. In comes Go channels. Channels provide a convenient, typed way to send and receive messages with the operator <-. Its usage is:
ch <- v // Send v to channel ch.
v := <-ch // Receive from ch, and assign value to v.
Let us use channels in our example:
package main
import (
"fmt"
"net/http"
)
type urlStatus struct {
url string
isUp bool
}
func checkUrl(url string, ch chan urlStatus) {
_, err := http.Get(url)
ch <- urlStatus{url, err == nil}
}
func main() {
urls := []string{
"https://www.easyjet.com/",
"https://www.skyscanner.de/",
"https://www.ryanair.com",
"https://wizzair.com/",
"https://www.swiss.com/",
}
ch := make(chan urlStatus)
for _, url := range urls {
go checkUrl(url, ch)
}
result := make([]urlStatus, 0)
for range urls {
result = append(result, <-ch)
}
for _, r := range result {
if r.isUp {
fmt.Println(r.url, "is up and running.")
} else {
fmt.Println(r.url, "is down !")
}
}
}
We declared a struct called urlStatus which simply contains two fields: the url and whether it is up or not. We will use this as the type for the communication in the channel. The checkUrl function has been modified to also accept a channel as an input. Instead of returning a result, we will send the result as an instance of urlStatus to the channel. We instantiate the channel with type urlStatus in the main routine and collect all results by receiving from the channel.
This shows the typed features of the channels and how they can be used to build some primitive Actor-Model like applications.
I hope this tutorial was helpful, and happy “Gophing”!
Originally published on Medium.