loading...

Concurrency in Go vs Erlang

pieohpah profile image Joe Chasinga ・5 min read

night view of cars on a bridge

I've been writing Go for over a year and have successfully written a reasonably big library.

However, I've been interested in functional programming recently, and very sporadically using Go to sketch up quick API servers. I started learning Erlang and had drawn some comparisons between these two in terms of writing concurrent programs.

Erlang is a pretty quirky language for many, especially programmers who are more familiar with C-style imperative programming.

We all know that Go is pretty popular not just because of its simplicity, but also because of its two concurrent constructs which knocks socks off Node.js--channels and goroutines. In Go, spawning a parallel routine and interacting with it is so trivial and can be done the following way:

func main() {
        numchan := make(chan int, 1)
        donechan := make(chan struct{})

        go func() {
                fmt.Println(<-numchan + 1)
                donechan <- struct{}{}  
        }() 
        numchan <- 8
        <-donechan
}

Point is a channel is a godsend passage to which you can send values from any goroutine and many other goroutines can consume it with no data race. A Go channel makes sure each of the routines asking for a value from it is curbed and line up nicely (it implements mutex and semophore under the hood). As a programmer, you just have to trust the channel. You have no control or knowledge of which routine gets to the channel first.

This isn't unexpected if you think about it. Each routine is executed (hopefully) in parallel and they shouldn't have to concern one another's business. Take this example when you have a few URLs that you need to distribute to several goroutines to act like workers, each printing the status code from the response it receives from the URL after it has finished sending a HTTP request:

func printStatusCode(url string) {
        res, _ := http.Get(url)
        fmt.Printf("%d: %d\n", pid, res.StatusCode)
}

func main() {
        var wg sync.WaitGroup
        urls := [...]string{
                "https://google.com",
                "https://facebook.com",
                "https://dev.to",
                // ... thousands more
        }
        for i, url := range urls {
                wg.Add(1)
                go func(pid int, url string) {
                        printStatusCode(url)
                        wg.Done()
                }(i, url)
        }
        wg.Wait()
}

Each goroutine prints out its assigned pid (which is just the index of the url in the urls array) and the status code of the response received. Printing the index out is interesting because it lets you see that the goroutines execute in parallel and not in a synchronous order. sync.WaitGroup is used instead of a signaling channel used in the previous example. It makes waiting for many goroutines easier.

The key takeaway here is Go abstracts concurrency by means of channels. You can use channels for sending data to one or more routines and for synchronizing/signaling between different routines.

Let's look at how to achieve the same thing in Erlang.

In Erlang, the parallel executions are called processes. They are the same as routines in Go (at least for the purpose of this post).

Erlang does not have an intermediary like Go channel, but it does employ a very powerful concept known as the Actor Model. In this world, a process is an independent actor. It doesn't care about the outside world. It's like a prisoner churning over its own thing and wait for something to be passed into its prison's door, or more specifically, mailbox.

A process's mailbox is akin to Go's channel, but it is not shared. It's a private data buffer for that particular process to go through, one by one, synchronously. When a process sends something to another process's mailbox, that thing gets stored there until that process can make use of it.

Here is a small example doing the same thing as the first Go example:

main() ->
    P1 = spawn(fun() ->
        receive
            Num -> io:format("~p~n", [Num + 1])
        end
    end),

    P1 ! 8,
    ok.

What is going on here? How could you achieve that with so little code? Well, for one, functional languages are designed one level higher than most imperative languages. In imperative programming (in Go's case, structured, which is imperative), you mostly program to change some states, and thus you have to care about WHEN each line of code gets run (A changes state X, then B reads X is consequentially different from B reads X, then A changes X). In declarative programming, one expresses the logic without caring about control flow because each expression / function does not have side effects aka changing the state outside of itself.

This is why Erlang's actor model is so simple and powerful. It never has to care about data race or syncing because each process can never access anything external.

Each Erlang process has a small memory footprint and can grow/shrink dynamically. It does not share memory and only communicate through message passing. This is very similar to Go's solution of concurrency. Here is another Erlang sample equivalent to the second Go code:

printStatusCode(Url) ->
    {_, Res} = httpc:request(Url),
    {{_, StatusCode, _}, _, _} = Res,
    io:format("~p: ~p~n", [self(), Code]).

main() ->
    inets:start(),
    Urls = [ 
        "https://google.com", 
        "http://facebook.com", 
        "https://dev.to" 
    ],
    lists:foreach(fun(Url) -> spawn(?MODULE, printStatusCode, [Url]) end, Urls).

Now this got a bit hairy, but it did so because of the Erlang's bracket tuple literal than the way it does things. What is prominent here is the underscore _ repeating in several places surrounded by brackets. This is known as pattern-matching, a technique common in the functional world. This is because = in Erlang means "bind" versus "assign to" in Go. Thus, you can match the left-hand shape of data with the right-hand one like this:

{X, Y, _} = {"Joe", "Jim", "Jam"},
%% `X` is bound to "Joe" and `Y` is bound to "Jim"
[F | _ | T | _] = [1, 5, 2, 10, 54, 0, 98].
%% `F` is bound to 1 and T is bound to 2 

Go has something close to this behavior, and it's called multiple returns.

// Aw, not even close :(
a, b := func() (string, string) {
        return "Joe", "Jim"
}()

Since Go doesn't have tuple expressions, it needs to wrap a multiple return values in a function expression. And you can't pattern-match an array or slice.

One difference to note is Go is a statically-typed, compiled language while Erlang is a dynamically-typed, bytecode-compiled language. Go would of course be more superior in arithmetic computation such as image processing than Erlang. And being compiled to executable Go program is more portable than Erlang without the need of a virtual machine.

Questions, feedbacks, and advices are welcome. Please leave them in the comments.

Posted on Nov 16 '19 by:

Discussion

markdown guide
 

One thing to mention, you brought up a good point that Erlang is bad at numerical linear algebra, computer vision, etc., which is why these types of routines in Erlang are actually C functions that are called from Erlang. This way, numerical calculations are actually run as C code

 

Good article, thank you. I always wanted to take the time reading up, how Go handles this. Just a few notes on Erlang, here:

First, I wanted to make the point Alexander did already. Erlang’s built in functions with a need for performance are written in optimized C. Also you can easily cross compile Erlang code to C and get pretty good performance.

And another thing: you second Erlang program has a bug: in line 4 you reference Code, but Code is unbound at that point.

Furthermore, just as a side note, the Erlang code is not idiomatic (function names should be snake_cased, deep pattern matching like in line 3 should be a descriptive call (e.g.

print_status_code(Url) ->
  %% ...
  {ok,StatusCode} = get_status_code(Res),
  %% ...

get_status_code({_,StatusCode,_},_,_}) -> {ok,StatusCode}.
get_status_code(UnknownFormat) -> {error, UnknownFormat}.

), and I personally wouldn’t take a foreach (ever) but map over the list).

 

Thank you for the corrections!

 

Good to read. You have me interested in functional programming.