Advantages of Elixir vs Golang

By Jake Morrison in Development on Tue 08 May 2018

A prospect recently asked me what the advantages are of Elixir over Golang.

The simple answer is productivity. You get the best of both worlds: the productivity of a high level language with the scaling power of the mature Erlang platform.

Go is a low level language, and performance is good, but it lacks the productivity features of modern languages. It was developed for making relatively low-level services at Google's scale, e.g. HTTP routing infrastructure. When you are operating at their size, you need performance, but the complexity of C++ is hard to deal with. You need concurrency, but multi-threaded network programming is error prone. I spent years making VoIP applications in C++, so I know this pain.

It is also a reaction to the tendency for smart C++ and Java programmers to create abstractions which make systems harder to maintain over time. The layers make it hard to jump into a big codebase and fix a problem. This is a particular issue for SREs who are not just programmers, they also have to keep on top of the challenges of cloud infrastructure, networking, storage, etc.

Higher level scripting languages like Python are hard to operate at scale. They suffer from poor performance and lack of concurrency. Dynamic typing makes it hard to avoid errors at runtime, requiring lots of testing. Dependencies make them difficult to deploy, so the ability to simply copy a binary to a server is very attractive.

Go is basically a simplified version of C++, a kind of "blue collar" language. The Go Programming Language describes it this way:

"The Go project includes the language itself, its tools and standard libraries, and last but not least, a cultural agenda of radical simplicity. As a recent high-level language, Go has the benefit of hindsight, and the basics are done well: it has garbage collection, a package system, first-class functions, lexical scope, a system call interface, and immutable strings in which text is generally encoded in UTF-8. But it has comparatively few features and is unlikely to add more. For instance, it has no implicit numeric conversions, no constructors or destructors, no operator overloading, no default parameter values, no inheritance, no generics, no exceptions, no macros, no function annotations, and no thread-local storage. The language is mature and stable, and guarantees backwards compatibility: older Go programs can be compiled and run with newer versions of compilers and standard libraries."

There is a certain "embrace the suck" attitude in Go. Deploying large systems at scale sucks, so we choose simple tools that will always work and push through the problems. I can understand this perspective, but I am not so cynical. It ignores the improved productivity and safety that we can get through modern programming language features.

Ericsson was seeing similar issues when they created Erlang, the basis for Elixir. They had been building their telephone switches in low level languages and it was getting out of control. The systems were complex, buggy, and expensive to develop. Their solution was a combination of a low-level runtime to handle the problems of networking and concurrency once and for all, combined with a high level language and framework to make programming easier.

Erlang's distinguishing features are concurrency and fault tolerance. The lightweight process model makes it straightforward to create systems which scale to millions of stateful connections, e.g. WhatsApp.

The platform has great depth of tools to create, debug and manage large production systems. The OTP framework standardizes patterns for building services out of components. The platform includes features like an in-memory key/value store, process registry, etc., as well as built-in solutions for issues like production system tracing, high volume logging, alerting and metrics.

Elixir starts with the mature Erlang platform and adds powerful language features like lisp-style macros and protocols. We get the ease of use of object oriented languages, without the tight coupling between components. Functional programming gives us generic algorithms which work across all data. Pattern matching makes logic simpler. Binary matching syntax makes it easy to implement network protocols reliably with high performance. Immutability and lack of side effects make systems easier to reason about and debug. Unlike academic functional languages like Haskell, the language is focused on practical industrial programming, not type theory. Data structures are straightforward and easy to understand. Everyone talks about the concurrency, because it's special, but the language itself is legitimately a joy to program in.

As an example, error handling in Go looks like this:

c, err := foo.Client()
if err != nil {
    panic(err)
}
msg, err := c.Request(foo, bar)
if err == nil {
    fmt.Printf("Message on %s: %s\n", msg.Topic, string(msg.Value))
} else {
    fmt.Printf("Client error: %v (%v)\n", err, msg)
    break
}

The equivalent code in Elixir would simply be:

{:ok, conn} = Foo.Client.connect()
{:ok, result} = Foo.Client.request(conn, foo, bar)

Elixir's pattern matching lets us program for the success case. If we get an error return (e.g. {:error, reason}), then the unhandled match will fail, and the thread will exit. It will write a backtrace to the log with all the context of the call so we can duplicate the problem in our dev environment.

A supervisor monitors the process and manages all errors, including ones we may have missed. This is different from exceptions, as it allows us to actually deal with errors, e.g. retrying a call on connection timeout.

Elixir is taking the opportunity to rethink and refine a well established system. Mature languages accumulate cruft over time. For example, Java has multiple date-time classes (java.util.Date, java.sql.Date, Calendar) and we have to convert between them. Functions have parameters in various orders, so we have to keep looking at the docs. When Microsoft created C# and .NET, they had the benefit of learning from Java, which helped them to quickly create a full, consistent standard library and virtual machine. Elixir does that for Erlang, but can also take advantage advantage of all the existing Erlang libraries.

Similarly, a lot of the Elixir community comes from Rails, as the creator of Elixir, José Valim, was a Rails committer. He started with the mature Rails system and did it again, better, focusing on the problems he had maintaining large Rails projects that had evolved over time. The platform has better performance and reliability, but also takes a step back on "magic", as some of the features which make simple projects easy end up causing problems when they grow bigger. In addition to standard MVC, the Phoenix web framework (http://phoenixframework.org/) provides a "channels" abstraction which makes it easy to create stateful web applications, e.g. web chat systems. There is a GraphQL server as well which integrates with Phoenix.

As with Ruby on Rails, programmer productivity and ease of use are a primary focus for the community. Everything works well out of the box, and there are standard, well integrated tools for testing, asset pipeline, deployment, etc. People have taken the libraries that they loved from Ruby and implemented them for Elixir. There are also cutting edge tools for static analysis and property testing coming from the academic community.

The sweet spot for Go

The most interesting applications of Golang for me are network services which need to be very fast, i.e. they are CPU bound. Garbage collection avoids a major class of errors, making it safer. Go has built in concurrency using the CSP model, and it can call into C libraries very efficiently. Good applications are things like deep content inspection or running machine learning models. When you are operating at huge scale, cost of hardware starts to actually be more important than programmer time.

The concurrency crisis

Most of us are not writing applications at the scale of Google, but we still need to efficiently use hardware we have. To do that, we need languages that can handle concurrency, but the current crop of languages and platforms face challenges. It is hard to make existing languages and libraries safe, as it breaks programmer assumptions. Network communication libraries need to be rewritten to be non-blocking or use threads. Worse, shared data needs locking to protect concurrent access. This has made it very difficult for existing scripting languages like Python, Ruby and PHP to support concurrency. Node.js is built around non-blocking IO, but can't take advantage of more than one CPU without hacks like multiple processes. There is too much exposed machinery, and the language lacks type safety. Java uses a similar thread-based approach to concurrency as C++, but pervasive object orientation creates potential locking problems for every object. Rust has potential as a safe replacement for C to do systems programming, with concurrency. It is too low level for general application development productivity, though.

Elixir has had support for concurrency from day one, and it has 30+ years of tooling developed for Erlang. While the absolute performance is less than compiled languages, it is easy to parallelize tasks to make use of the machine. If that's not enough, we can deploy the app to work across a cluster of servers with few changes. This is the difference between speed and scalability. Go focuses on low level performance and relies on systems like Kubernetes to scale. That has its own nest of complexity to deal with, though.

It is easier to add libraries to Elixir for practical web programming tasks than it is to make other systems concurrent and reliable. We are using it to build large systems today, and we know it works. Instead of fighting to retrofit concurrency to existing languages, we can get on building the next generation of systems.