Is Go The Right Choice for Building a Backend?

11 min read
Annis Souames
Annis Souames
Published August 21, 2024

Since its creation by Google in 2007, Go (Golang) has emerged as a strong contender in the space of backend development, thanks to its simplicity, performance, robust concurrency support and other benefits. But with so many languages attracting developers' attention, is Go the right choice for your next project?

In this article, we'll explore Go's features from its birth to its current status, its strengths and weaknesses, and how it stacks up against other popular backend languages to help you make an informed decision while building your backend and understand why it has been a great technology to use at Stream.

This isn't your typical "Go Pros and Cons" list article. We'll cover Go's historical context and provide insights from key talks by experts who shaped the language.

The Inception of Golang

The design of Golang is quite ingenious and did not happen by mere coincidence. The language's inventors; Rob Pike, Ken Thompson, and Robert Griesemer; are highly respected names in the programming world, with extensive experience in designing systems, programming languages, and compilers. Griesemer had good experience with building JVMs and working on Google's V8 JavaScript engine before starting the Golang project. Pike and Thompson contributed their substantial expertise in operating systems, with Ken Thompson being the original designer of UNIX, and Rob Pike having developed the Plan 9 operating system and several programming languages (and the UTF-8 encoding!). This collective expertise meant that Go started off with a solid team that knew what it was doing while building the language.

Go was created to offer developers at Google a fast, statically typed, and compiled programming language with robust support for concurrency, simplicity in writing and reading code, and a safe memory management model. This language helped engineers at Google be more productive while working on technologies that dealt with large clusters and high-load systems. Over the years, Go has grown to become a simple, robust, and performant language used by over 1.1 million developers across the world, according to a 2021 survey from Jetbrains.

Go was designed with a clear vision by industry experts to address the shortcomings of existing languages and meet the demands of modern software development at scale. In fact, some of the most popular infrastructure and cloud technologies were written in Go: Kubernetes, Docker, Terraform and Prometheus are all tools built in Go and used to build infrastructures at scale. For more popular projects written in Go, checkout this list.

Why is Go Well-Suited for Backends?

Go has many features that make it great for building backends, whether they're small and simple or big and complex. Let's look at some things that make Go stand out from other languages for backend work.

When building a complex backend, you need a language that can quickly compile your code and all its parts into an executable your server can run and Go excels at this.

As a compiled language, Go stands shoulder-to-shoulder with low-level languages like C and C++. Unlike interpreted programming languages such as Python or JavaScript, or JVM-based languages like Java, Go compiles directly to machine code, eliminating the need for interpreters or virtual machines at runtime. You write code in Go and the compiler compiles it into binary code that is understandable directly by the machine.

Additionally, Go compiler is blazingly fast even when compared with other compiled languages, and this for a few good reasons:

  • Go's approach to dependencies is better designed than many established C and C++ compilers. It enforces a clean, acyclic dependency graph by disallowing circular and unused imports, resulting in a clean dependency-tree (and not a graph, since there are no cycles) structure that's far more efficient to traverse.
  • Go is a minimal language and lack pre-processors, this allow for more efficient parsing compared to C++ or Java. The current stable version of Go at the time of writing this article (v1.22) has about 25 keywords only: no generics, no try-catch blocks, just the essential keywords. This helps the parser tremendously in parsing the code fast.
  • Finally, to make the compiler even faster, the object file in Go is structured with the export data positioned at the beginning. This arrangement allows the compiler to stop reading the file as soon as it finishes processing the export section, improving compilation speed.

The folks at Stream use Go extensively, and wrote a brilliant article on how the Go compiler translate you Go code into machine code in the blink of an eye. Additionally, if you are interested in comparing compilation speeds between Go and C, checkout this benchmark by Stackoverflow.

Concurrency Has Never Been Easier

Google invented Go, and this should tell you that Go is probably good for building large-scale applications. This isn't just a wild guess, it is a fact. The language makes it straightforward to write code in a concurrent fashion, right from the start. Rob Pike, one of the inventors of the language, emphasized in his talk "Concurrency is Not Parallelism" (which I highly recommend watching), that Go was designed to make concurrency easier to deal with for developers through go-routines and various mechanisms available through the sync package in the standard library.

Go employs the concept of Goroutines, which are lightweight, user-space threads. Unlike traditional threads, Goroutines are extremely lightweight, allowing you to run hundreds of thousands of them within a single application. They scale efficiently, making it easy to manage concurrent tasks. In addition, to facilitate communication between Goroutines, Go uses channels. This concept was introduced by British computer scientist and Turing Award winner Tony Hoare in his pioneering paper "Communicating Sequential Processes" (CSP). Channels enable safe and structured communication between Goroutines, making it simpler to build concurrent applications.

Besides channels for communicating between concurrent parts of your code, the sync package of the standard library has a good set of tools to easily deal with concurrency, such as: Mutexes (Mutual Exclusions), Wait Groups, Synchronized Maps, etc. These mechanisms make a Go an first-class language for building backend with concurrency in mind that can scale well on multiple cores and multiple machines.

One Standard Library to Rule Them All

Honestly, I am quite impressed at how comprehensive the standard library for Go is. For instance, if you're coming from Python, you're likely accustomed to reaching for third-party packages for common tasks. Want to run a web server? You'll need Flask or Django. Sending HTTP requests? Time to install requests. The situation is even more challenging with lower-level languages like C or C++ where you need to go through a complex process to integrate them with your project.

On the other hand, Go's standard library provides a wealth of packages that are sufficient for building sophisticated applications:

  1. net/http: A full-featured HTTP client and server implementation.
  2. encoding/json: Efficient JSON encoding and decoding.
  3. database/sql: A generic interface for SQL databases.
  4. image: Basic image processing capabilities.
  5. time: Robust yet simple time and date handling.
  6. sync: Primitives for concurrency and writing thread-safe code. ² Don't get me wrong, though, third-party packages are still important in Go as in any other modern language, and you will need them for specialized tasks. Fortunately, Go has a large community that built some nice Go packages such as gin and ollama, you can find a list here of different Go libraries and frameworks for several categories. In short, although Go has a large and active package ecosystem, the packages offered by the standard library allow you to build a functioning application that is consistent and performant while keeping the dependency tree minimal and clean.

Write Once, Run Anywhere (for real this time)

As a backend developer, the problem of "but it runs on my machine" is all too familiar. Sure, Docker (also built in Go) has largely resolved this issue, but Go takes it a step further by addressing the root cause.

Go's approach to deployment is plain and simple. When you compile a Go program, it bundles everything into a single executable file. This means deploying your application is as straightforward as copying that one file to your server. No fuss, no muss. Compare this to the deployment process for a couple of other languages commonly used to build backends:

  • Python requires the Python interpreter and all your third-party libraries to be installed.
  • Node.js needs the Node runtime and your npm packages.
  • Java and .NET rely on their respective virtual machines and a correctly configured environment.

Go cuts through the complexity of whether the target machine has the right version of the runtime or if all the necessary libraries are installed. Your Go executable is self-contained and ready to run.

Similarly, building a go project is painless and free of hassle, there is no need to deal with complex building toolchains like in Node.js with package.json and webpack, or Makefiles, Maven configurations and setup.py files. All you need is to run go build anywhere in your project tree, and you're done. It's that easy. Go's approach to building and deploying makes it a loved language amongst DevOps and developers alike.

No Language is Perfect

As we've discussed earlier, Go has many strengths that make it an excellent choice for backend development. However, it's important to understand that no language is perfect. The notion of the absolute best programming language is just an illusion. Each language has pros and cons, and Go is no different. I will present some issues that Go developers face that are worth noting.

Forget About Try/Catch

Go's approach to error handling, while simple and explicit, can sometimes lead to verbose code. Unlike languages with exceptions and try-catch blocks, Go requires errors to be checked and handled explicitly:

f, err := os.Open(fileName)
if err != nil {
    return err
}

Here, the error is treated as a value and has to be checked with an if/else block. Nevertheless, the lack of a try/catch block has been justified by Rob Pike in his talk "Go at Google".

Errors are just values and programs compute with them as they would compute with values of any other type. It was a deliberate choice not to incorporate exceptions in Go. Although a number of critics disagree with this decision, there are several reasons we believe it makes for better software.

-Rob Pike, Co-Inventor of Go

Thus, error handling in Go is simple yet verbose, which makes it a bit of a controversial topic among developers. I personally enjoy this approach more than a try/catch block; maybe because the very first programming language I touched was C were error handling was quite similar to Go's philosophy.

Limited Abstractions

Historically, Go was commonly criticized for its lack of generics. This feature was introduced in version v1.18. However, Go still lacks some features and abstractions that are common in OOP languages. If you are coming from languages such as Python, Java or even JS/TS, you will find yourself forced to write more boilerplate code. Go doesn't have classes or inheritance in the traditional sense. Instead, it uses structs and interfaces to achieve similar functionality. Many developers have criticized this limitation of Golang, although it favours composition over inheritance, it can make large code bases hard to maintain with time.

To highlight this issue, consider the following example in Go:

type Animal struct {
    name string
}

func (a Animal) Speak() {
    // This method can't be overridden in a "subclass"
    fmt.Println("Some generic animal sound")
}

type Dog struct {
    Animal  // Composition instead of inheritance
    breed string
}

func main() {
    dog := Dog{Animal{"Fido"}, "Labrador"}
    dog.Speak()  // Outputs: "Some generic animal sound"
}

In this example, we just want to have an Animal that can speak and then create a Dog based on that Animal "abstraction". In Python or Java, this can easily be done with classes, methods and inheritance. In Go, it's a different story: you can't create a true "subclass" of Animal. Instead, we use composition by embedding Animal in Dog making the code more verbose. Amos Wanger, a previous engineer at Netlify, wrote at length about this limitation and some other cons of Golang: Lies we tell ourselves to keep using Golang (fasterthanli.me)

Go is a relatively new language compared to other languages such as Java and Python, it has only been around for about 15 years, and is growing rapidly. This growth essentially means two things:

Go can sometimes feel a bit unstable between versions. Some developers have reported issues when running older code with newer Go compilers. This volatility is likely a growing pain that should decrease as the language matures and stabilizes, just as teenagers become mature and more reasonable when they grow up eventually.

The job market for Go developers isn't as robust as for more established languages. That said, don't count Go out - many major companies are using it and looking for talented Go engineers:: Google (Obviously), Microsoft, Meta, Dropbox, Hashicorp, Docker, Soundcloud, Twitch, Capital One, American Express, and getstream.io to name a few, however, when speaking in relative terms, other languages have more job opportunities, look at the following chart from Google Trends for the last year (Aug 2023 - Aug 2024) to understand how Python compares to Go in terms of popularity.

That being said, Go has definitely potential, it just didn't rule the high school of programming languages yet.

                                                Python v.s Golang on Google Trends (2023 - 2024)

Parting Thoughts

Go at Stream

A couple of years ago, Stream wrote a case study explaining why they moved their backend stack from Python to Go. Since Stream has to offer real-time features for developers working on chat and video calls that have millions of end users, speed and performance are key to the success of Stream.

According to the author, Stream's move to Golang brought a significant boost to their performance. For benchmarking purposes, one of the features was written in both Python and Golang, the code in python took 3 days to write, while the Go version took 4 days, however the feature built in Go ran 40 times faster than the Python version, even after optimizing the Python-built feature. Go concurrency model, static typing, and fast compiler helped the company handle massive numbers of concurrent requests coming from over 500 million users, happy and chatting away.

Verdict: Should you move to Go?

One thing you learn when as you grow into a professional software engineer is that there is no such thing as the "perfect technology", no silver bullet that solves all your problems. We've explored what makes Go shine and why it's the go-to choice (sorry for the pun) for companies working on large-scale problems like Google, Meta, or Stream. Ultimately, the decision is yours to make, based on your specific environment and the problems you're trying to solve. Nevertheless, here is a simple set of questions you can ask yourself before jumping ship:

  • Do I have enough experience in Go to work on production-level code?
  • Do my colleagues/employees/other people have experience in Go?
  • What's the cost - in both cash and sweat equity - for making the switch?
  • What's my top priority? Is it performance? Latency? Developer experience? Scalability?

These are a handful of questions you can ask yourselves before moving to Go (as well as before adopting any other new technology in your stack). It's also recommended to try to code a single feature or part of your code in Go and measure both the effort it takes and the results you get. If you see some serious benefits, then it might be time to consider Go for your next project or migrating your current one to the language.