Go uses goroutines to execute multiple bits of code at the same time. Channels allow for the aggregation of the results of these concurrent calls after they have finished.

Consider a case where we want to make several GET requests to a server. The server takes some time to process each request, in many cases can handle many simultaneous connections. In a language like Python, we might do the following to make several requests:

client.py
import requests
import time

start = time.time()
for _ in range(10):
    r = requests.get('http://localhost:8080/inc')
    print r.content
print('Time elapsed: %.2f seconds' % (time.time() - start))

If the server takes an average of 100ms to respond, it will take us about one second to do ten requests in native Python.

Let’s run the above Python code against the following Go HTTP server. The server prints the (random) request delay and a global counter it maintains to keep track of how many requests have been made. It responds to the caller with the string: “The count is count”.

server.go
package main

import (
    "fmt"
    "log"
    "math/rand"
    "net/http"
    "time"
)

var counter int

func main() {
    http.HandleFunc("/inc", func(w http.ResponseWriter, r *http.Request) {
        duration := time.Duration(rand.Float64()*200) * time.Millisecond
        fmt.Println("Sleeping for: ", duration)
        time.Sleep(duration)
        counter++
        msg := fmt.Sprintf("The count is: %d", counter)
        fmt.Println(msg)
        fmt.Fprintf(w, msg)
    })

    log.Fatal(http.ListenAndServe(":8080", nil))
}

We run the server to accept incoming requests:

go run server.go

Then we run the Python script:

python client.py

which outputs the following:

The count is: 1
The count is: 2
The count is: 3
The count is: 4
The count is: 5
The count is: 6
The count is: 7
The count is: 8
The count is: 9
The count is: 10
Time elapsed: 0.96 seconds

Our server output looks something like this:

Sleeping for:  120ms
The count is: 1
Sleeping for:  188ms
The count is: 2
Sleeping for:  132ms
The count is: 3
Sleeping for:  87ms
The count is: 4
Sleeping for:  84ms
The count is: 5
Sleeping for:  137ms
The count is: 6
Sleeping for:  13ms
The count is: 7
Sleeping for:  31ms
The count is: 8
Sleeping for:  19ms
The count is: 9
Sleeping for:  60ms
The count is: 10

The total elapsed time is about what we would expect. Ten requests at approximately 100ms each, gives us about one second for all requests. However, our Python script spends most of its time waiting for a response from the server which is sleeping. What would happen if we kicked off all ten requests to the server at the same time? We might expect this to be a problem. After all, if Python only can make one request at a time, why should our server be able to process more than one request at a time. For this to work in Python we need to use something like uWSGI or asyncio. It turns out that Go’s builtin net/http library uses goroutines to handle multiple incoming requests at once. Let’s trying making our requests in a similar manner, using goroutines to kick off all the requests at once, with the following Go code:

client.go
package main

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

func main() {
    start := time.Now()
    routineCount := 10
    // create a go routine to make the HTTP request
    // pass a channel into which the response will be written
    channel := make(chan string)
    for i := 0; i < routineCount; i++ {
        go request(channel)
    }
    // read the responses from the channel
    for i := 0; i < routineCount; i++ {
        fmt.Println(<-channel)
    }
    secs := time.Since(start).Seconds()
    fmt.Printf("Time elapased: %.2f seconds", secs)
}

func request(channel chan<- string) {
    resp, err := http.Get("http://localhost:8080/inc")
    if err != nil {
        fmt.Println(err)
        return
    }
    defer resp.Body.Close()
    body, _ := ioutil.ReadAll(resp.Body)
    channel <- fmt.Sprintf(string(body))
}

When we run this code against the same (restarted) server, we get the following output:

$ go run threads.go
The count is: 1
The count is: 2
The count is: 3
The count is: 4
The count is: 5
The count is: 6
The count is: 7
The count is: 8
The count is: 9
The count is: 10
Time elapased: 0.19 seconds

With goroutines, the program runs about five times faster than our ten synchronous requests in Python. Look what happens on the server side:

Sleeping for:  87ms
Sleeping for:  132ms
Sleeping for:  120ms
Sleeping for:  188ms
Sleeping for:  84ms
Sleeping for:  137ms
Sleeping for:  13ms
Sleeping for:  31ms
Sleeping for:  19ms
Sleeping for:  60ms
The count is: 1
The count is: 2
The count is: 3
The count is: 4
The count is: 5
The count is: 6
The count is: 7
The count is: 8
The count is: 9
The count is: 10

This time on the server side, all the sleep durations are printed first, then the counter is incremented afterwards. So what happens is the server accepts all ten requests then all threads start sleeping. As each of the threads wakes up, they start incrementing the counter respectively and returning to the client callers. The responses on the client side look about the same as they did when we made the requests in series, but this time the total time elapsed was only 0.19 seconds. This corresponds to the longest sleep time printed by the server: 188ms. So, by using goroutines, we have reduced the runtime of our program from the sum of the time of all requests to the time of just the longest request. Not bad.

Another cool part about using goroutines in this scenario is that we can scale the number of threads to accomplish even more in the same amount of time. Keep in mind, even though creating goroutines is cheap, creating too many of them to make a large number of requests against an HTTP server all at once may cause the server to run out of resources or limit the number of connections it will accept. On my machine, scaling up routineCount to 300 is no problem for the server. However, at around 400, some of the requests start getting lost and at 800 I start seeing the following error:

Get http://localhost:8080/inc: read tcp [::1]:59928->[::1]:8080: read: connection reset by peer

From a more detailed explanation on what happens when we make too many concurrent requests, check out this Stack Overflow post.