Go is Boring...And That’s Fantastic!
A deep dive into why the world depends on simple, reliable, well-understood technologies
June 22, 2020
I've been working as a professional software engineer for nearly 23 years, and I’ve been writing programs for about 38.
In that time, I've used a lot of languages. I love programming languages and learning about their new features and what changes they've made compared to the languages that came before them.
And then there's Go.
The best way to think about Go is to think about all the things that it doesn't have.
- Go doesn’t have a virtual machine or an LLVM-based compiler.
- Go doesn’t have exceptions.
- Go doesn’t have user-defined implementation inheritance.
- Go doesn’t have overloading for functions, methods, or operators.
- Go doesn’t have immutables.
- Go doesn’t have enumerations.
- Go doesn’t have generics.
- And Go hasn’t added any major features since Go 1 was released in 2012.
The one exciting thing that Go has is built-in concurrency support via goroutines, channels, and select. However, it is based on ideas from CSP, communicating sequential processes, which was first described in 1978.
This doesn’t sound like a programming language from the 21st century, does it?
And yet, Go is the 3rd most wanted and (maybe not coincidentally) the 3rd highest paying language, according to Stack Overflow. Every startup in Silicon Valley is using Go to build their infrastructure. Docker, Kubernetes, etcd, Terraform, Vault, Consul, Traefik and lots of other cutting-edge projects are written in Go. So what's going on? Why is everyone interested in this boring language?
Why Software Engineering is the New Bridge Building
So before we answer that question, let's step back for a bit.
This is the Arkadiko Bridge in Argolis, Greece. At just over 3,000 years old it's the oldest bridge in the world that's still standing today. Amazingly, it's still in use.
Now, why should we care about an old bridge? It's because there's a universal truth about software development that software engineers don't like to talk about too much.
We're really bad at writing software.
And I don't just mean that one person in the office who your manager sends out on coffee runs to reduce the bug count at crunch time. I mean everyone - me, you, and every famous developer you can think of.
But the people who design and build bridges, they're great at it. Bridges get built on time, on budget, and last for dozens, hundreds, even thousands of years. Bridge building is, if you think about it, kind of awesome. And bridges are such a common occurrence that they’re also incredibly boring. No one is amazed when a bridge works correctly, and everyone is kind of amazed when software does.
Unfortunately, the world is very dependent on software. It might even depend more on software than it does on bridges. So we have to get better at writing software far faster than we got good at building bridges.
Everything We Know About Writing Software
There are a few things that we have learned in the last 60 years about writing programs where there is pretty much universal agreement:
- We agree that it's better to find problems earlier rather than later.
- We agree that people are awful at managing memory in programs.
- We agree that code reviews help find bugs.
- We agree that on any project that requires more than one person, communication costs dominate.
Hardware Isn’t Saving Us
We can combine these few things that we know with another truth that has settled in: computers aren't getting faster any more. At least not like they used to. During the 1980's and 1990's, CPUs got twice as fast every 1-2 years. That's changed.
When you look at single core performance, the fastest 2019 Core i9 is less than twice as fast as the fastest 2011 Core i7. Instead of getting faster, we're adding more cores to CPUs. When you look at multi-core performance, it's a little better, slightly more than 2x faster.
It’s not just CPU performance that is limiting us. Forrest Smith wrote a fantastic blog post on the impact of RAM and RAM access patterns on performance. The highlights are:
- RAM is much slower than CPUs, and the gap is not getting better, even though CPUs aren't getting much faster.
- RAM might be random access, but if you actually use it that way, it's slow. You can read around 40 gigabytes per second from RAM on a modern Intel CPU if the data is sequential. If you do a random read, it’s a little less than half a gigabyte per second.
- Code with lots of pointers is especially slow. Quoting Forrest: “Sequentially summing values behind a pointer runs at less than 1 GB/s. Random access, which misses the cache twice, runs at just 0.1 GB/s. Pointer chasing is 10 to 20 times slower. Friends don't let friends use linked lists.” Ouch.
Boring is the New Exciting
So, given these few, precious things that we know about how to build software and the hardware that we have available to us, let's take another look at Go.
Go and Software
Finding Problems Earlier Rather Than Later
The Go language might lack features, but it ships with a great set of tools. Go's compiler is fast, and that fast compilation speed is considered a feature by the Go team. It lets you quickly see if your code compiles, and if it doesn't, it lets you see where the problems are. Testing is built into the standard library to encourage developers to test their code and find problems. Benchmarking and profiling and race checking are included out of the box, too. Very few languages ship with these tools and they make it easier to find problems quickly.
As we all know, Go has a garbage collector. You don't have to worry about keeping track of memory and that's a fantastic thing. Among compiled languages, garbage collection is rare. Rust's borrow checker is a fascinating way to get high performance and memory management, but it effectively turns the developer into the garbage collector, and that can be hard to use correctly. Swift's ARC can still leak memory if you make mistakes and forget to declare some references as weak. Now, Go's GC isn't as performant as these semi-automatic systems, and there are some situations where you need the extra speed, but in most cases it's certainly sufficient.
Code reviews are important if they are done well. In order to have an effective code review you need to make sure that the reviewers are focused on the right things. Low-quality code reviews spend time on things like formatting. Go helps here because there are no formatting arguments when reviewing Go code, because all Go code is formatted the way that go fmt says code should be formatted.
And code reviews are a two-way street. If you want a review that works well, you need to make sure that other people can understand your code. Go programs are supposed to be simple, using a few well-understood constructs that haven't changed since the language was released. Because there are no exceptions, aspect-oriented programming, inheritance and method overriding, or overloading, it's very clear what code is calling what and where the values are returned. If you stay away from modifying package-level variables in Go it's very easy to see exactly how data is being modified. Since Go has changed so little, you can avoid the lava flow anti-pattern, where you can tell exactly how old some code is based on when a feature it uses was introduced into the language.
How does Go help with this? We've already talked about how Go's simplicity, stability, and standard formatting makes it easier to communicate what your code is doing. And while that's part of it, there's something else as well. Go's implicit interfaces help teams write decoupled code. They are defined by the calling code to describe exactly what functionality is needed, which clarifies what your code is doing.
Go and Hardware
The decision to make Go a compiled language has paid off. Interpreted languages running in virtual machines seemed like a good idea when CPUs were getting faster every day. If your program wasn’t fast enough, just wait a year and it’ll be fine. That doesn’t work anymore. Compiling to native code is a lot less interesting than the latest virtual machine tricks, but it gives a big performance advantage.
Let’s compare Go’s performance to some languages that run in Virtual Machines using microbenchmarks from The Benchmark Game. First we’ll look at Python and Ruby compared to Go. Any percentage less than 100 means faster than Go, greater than 100% means slower:
That’s a lot of red. There’s one benchmark where Python is faster (oddly, it’s not only twice as fast as Go, it’s faster than every other language at this one test) and none where Ruby is. Except for that one case, both languages produce code that is between 17% slower and over 60 times slower than Go.
Java and Go are pretty close in performance. Java is faster than Go in four cases, about the same in two cases, and slower in four cases. The worst that Go does is about three times slower than Java, the best Go does is about 50% faster.
What we are seeing is that the only VM that can keep up with Go is Java's. Hotspot is amazing technology, but the fact that you need one of the best engineered pieces of software in the world in order to break even with a compiler that prioritizes compilation speed over optimization says something. And you pay a price for that amazing technology. The memory usage of Java applications is many, many times larger than Go applications.
There's a second advantage too. The garbage that garbage collectors manage are pointers that aren't in use. Unlike languages that hide their pointers, Go gives you control. It lets you avoid pointers and lay your data structures out in a way that allows fast RAM access. This also allows Go to use a simpler garbage collector because Go programs simply create less garbage. Being boring is just less work.
And as we all know, CPUs are making up for their lack of increased speed with more cores. So it's good to use a language that takes advantage of this.That's where Go's concurrency support comes in. Having language-level support for concurrency and a runtime library that schedules goroutines across multiple threads means that when you have multiple CPU cores, those threads can be mapped to those cores.
I Do Not Want What I Haven’t Got
We have seen that Go focuses on the features and tooling that we know make it easier to create software and that better fit the memory and CPU architecture of modern computers. But what about the features that other languages have and Go doesn’t? Maybe Go developers are missing out and those features that Go doesn't have help developers write less buggy, easier to maintain code. Well, it turns out that as far as researchers can tell, that's not the case.
A paper in 2017 called “A Large Scale Study of Programming Languages and Code Quality in Github” looked at 729 projects, 80 Million SLOC, 29,000 authors, 1.5 million commits, in 17 languages to try to answer the question: What is the effect of programming languages on software quality? Their answer was that it makes little difference:
“It is worth noting that these modest effects arising from language design are overwhelmingly dominated by the process factors such as project size, team size, and commit size.”
Another group of researchers took a second look at this data and did a reproduction study in 2019 called “On the Impact of Programming Languages on Code Quality”. What they found was even more surprising:
“Not only is it not possible to establish a causal link between programming language and code quality based on the data at hand, but even their correlation proves questionable.”
If programming language choice doesn’t matter, why choose Go? What these studies show is that process matters. Tooling, testing, performance, and ease of long-term maintenance are more important than trendy features. When properly used, Go’s built-in tooling supports better processes while providing time-tested features.
This isn’t to say that new features are bad. Bridge building technology has certainly advanced over the centuries and millennia. But would you want to be the first to go over a bridge that was built with brand-new ideas and untested technology? You'd want to wait a little bit and have people test it out before you adopt it.
The same is true for software. If we're going to build software infrastructure that's as reliable as bridges, we're going to need to use software technologies that are as well-tested and well-understood as those we use for physical infrastructure. That's why Go is mostly using features designed in the 1970s. We know they work.
Go is boring, and that's fantastic. Let's all go out there and use it to build tomorrow's exciting applications.
Gopher Mascot Generated with Gopherize.me
Bridge image: "Arkadiko Mycenaean Bridge II" (https://commons.wikimedia.org/wiki/File:Arkadiko_Mycenaean_Bridge_II.JPG) by Flausa123 (https://commons.wikimedia.org/w/index.php?title=User:Flausa123&action=edit&redlink=1) is licensed under CC BY-SA 3.0 (https://creativecommons.org/licenses/by-sa/3.0).