Blog

Concurrency comparison of Javascript to Go

A lot has been written on concurrency and it is a deep subject. In order to understand the subtlety of how concurrency is implemented in Go and Javascript respectively, consider them through this high-level, relatively simple example.

For deeper understanding of how coroutines, channels, and generators work I will include some links at the bottom.

Mixing Board

Example

Simple Async / Await for an HTTP request. The goal is to wait until we have the data, but not block the main execution context while we wait.

Javascript

const axios = require('axios');

(async function() {
    console.log('here')
    const str = await axios.get("https://google.com")
    console.log('there')
    console.log(str.status)
    console.log('everywhere')
})()

console.log('also here')

With output:

here
also here
there
200
everywhere

Go

package main

import (
    "fmt"
    "net/http"
)

func get(url string, c chan int) {
    resp, _ := http.Get(url)
    fmt.Println("there")
    c <- resp.StatusCode
}

func main() {
    c := make(chan int)
    fmt.Println("here")
    go get("https://google.com", c)
    status := <-c
    fmt.Println("everywhere")
    fmt.Println(status)
}

with output

here
there
everywhere
200

In both of these examples, the main thread is not blocked

Let’s explore what that means:

In javascript, we see the rest of the program executing while the HTTP request is being made. Note the “also here” log order. With the await we are avoiding callback hell by blocking the current function execution without blocking the overall process execution.

In Go, everything in the current context stops while we wait for the HTTP request. This is because the main function waits for the return from our channel. But the thread is still available to the program as a whole see this explanation under “Blocking is fine”. This is a result of the Go runtime and the core scheduler. It’s very subtle the way that the main thread is blocked, but this is expected and encouraged behavior in Go because there is a top level language construct to schedule multiple processes in multiple threads and intelligently manage suspended contexts along the way.

In the case that we need to do something else while we wait for the HTTP request to return, we can modify this example to the following:

package main

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

func get(url string, c chan int) {
    resp, _ := http.Get(url)
    fmt.Println("Everywhere")
    c <- resp.StatusCode
}

func main() {
    fmt.Println("here")
    c := make(chan int)
    go get("https://google.com", c)
    fmt.Println("there")
    for {
        select {
        case status := <-c:
            fmt.Println(status)
            return
        default:
            fmt.Println("Also Here")
            time.Sleep(1000 * time.Millisecond)
        }
    }
}

With Output:

here
Also Here
there
200
everywhere

In this case, we loop in the main function using for { select {.... The main function will hit the default case over and over while waiting for the return value from our spawned channel. We can spawn other coroutines and add to the cases in the select, or write additional procedural code in the default case. In Go this is an expected pattern to allow the language scheduler to handle these routines and potentially multiple threads along the way.

A note on resource limitations.

Remember that Javascript is single-threaded. Concurrency is based on coroutines. Go is multi-threaded, but a coroutine does not always mean a new thread. See the Go FAQ for more details.

Conclusion

Concurrency is important, and coroutines make concurrency possible on fewer resources. Javascript and Go both have schemes to make concurrent, non-blocking, executions more readable and tolerable. The differences are subtle but result in two tools with a robust ability to execute a lot of async calls on fewer resources.

Reading List

Categories: Blog

Tags: , ,

Jeff Adams
17 Dec, 2019