Despite having worked for so many years with colleagues who were major proponents (or even contributors) to Go/Golang, I’d somehow gone this long without having ever written a production grade service in the language, having only dabbled so far with toy projects, small scripts, and minor contributions to existing software. That all changed in the last few weeks, where I had the opportunity to assemble a pretty serious project in the language from scratch.
I took notes throughout the process so as not to lose my (relatively) fresh outlook on the language.
Building new programs requires typing a lot. The language is incredibly verbose and has few shortcuts. The upside is that once you have typed out that initial code, it’s eminently readable and relatively easy to maintain compared to many other languages.
All too often, the bar to understanding projects written in Ruby, Lisp, Haskell, Rust, C#, C++, or whatever, isn’t just figuring out the code, it’s also deciphering the localized (and often overcomplicated) abstractions that every developer ends up baking into their code to reduce line count, and in many cases that’s significant cognitive overhead. In Go that problem doesn’t exist to anywhere near the same extent.
After spending some time with them, I’m firmly convinced that green threads (Goroutines) and channels are the only way that concurrency should be exposed to developers.
When working in languages like Ruby (to pick an example of one that I’m very familiar with), even with experience, doing any work with concurrency is incredibly frustrating in just the sheer number of problems that you’re likely to run into. It’s tempting to think that this is because concurrency is inherently difficult, but it’s more to do with dull primitives that are error prone by default.
By comparison, when working in Go, it’s amazing how many programs you can write where your concurrent code will work perfectly the first time. I also find that even in cases where it doesn’t, it’s far more often due to a conceptual mistake that I’ve made than it is to a poorly designed language feature.
I also appreciate just how opinionated the Go team has been on this front. Other languages with strong concurrency stories like Haskell and Rust have opted to give users access to every type of primitive under the sun, and in the long run that tyranny of choice leads to an ecosystem of mixed ideas and no clear way forward.
Speed is absolutely critical, and not just for the runtime, but for the tooling. Being able to compile and run your entire test suite in under a second 1 changes the entire development experience in such a fundamental way that it’s hard to adequately describe. After working with a Go project for a while, going back to 10+ second iteration loops in languages like Ruby feels like trying to run a marathon while waist deep in a bog.
This is still a key place where Go stands apart even from other modern languages which tend to focus on runtime speed or sophisticated features while ignoring the development cycle 2.
But Go is also fast at runtime too. It’s nice to be able to write code in a high-level language and be able to trust that it will run quickly.
If every language was as easy to deploy as Go, Docker would never have been invented (maybe a slight exaggeration, but the need for it wouldn’t have been anywhere near as dire). Build a binary. Copy it to a target server. Stop the old service and bring the new one up. That’s it. No weird environment problems. No dependency headaches. No Bundler.
I now write all my throwaway scripts in Go for the same reason. If I ever need
to run one with Cron, I know that I’m never going to have to deal with issues
$PATH or rbenv or anything else. Copy the executable to
/usr/local/bin, inject it straight into my Crontab, and you’re done.
killall even works; incredible.
There’s a lot to like about Go:
using block (in that you might accidentally remove the line and
not notice), it’s far less cluttering.
http from “net/http”) and then having
only one option for referencing that package in code is the One True
Way. No more symbols with unknown and dubious origin (Haskell) or artisanal
blends of qualified and non-qualified names (C#/Java/other).
default: to make a
blocking or non-blocking are a little obtuse, overall this construct is
goto is also tremendously
powerful (and comes with the perfect number of restrictions to prevent its
There were a lot of facets of Go that I read or heard about before trying and which I was pretty sure that I wouldn’t like. However, after using the language a while I quickly started warming up to them:
I really did make an effort, but even so, some parts of the language are hard to love:
make, and initialization with composite
select blocks with a
default case become non-blocking.
nil is allowed by the compiler, but apparently
not a good idea, and can lead to some strange bugs.
There are very few parts of the language that are unapologetically bad, but that said:
The real problem though is that the unnecessary verbosity of tests acts as a natural deterrent to writing them, and projects with non-existent/poor/incomplete test suites are a dime a dozen. I’ve been using the testify require package to ease this problem, but there should be answer in the standard library.
Overall, I never quite reached the level of feverish passion for Go that many others have, but I consider it a thoroughly solid language that’s pleasant to work with. Furthermore, it may have reached the best compromise that we’ve seen so far in the landscape of contemporary languages in that it imposes constraints that are strong enough to detect large classes of problems during compilation, but is still fluid enough to work with that it’s easy to learn, and fast to develop.
1 Without tricks like Zeus that come with considerable gotchas and side effects.
2 e.g. Rust, or, and it hurts me to say this, Haskell.
3 The best single example of this that I’ve found so far is a request for a non-zero exit code in Golint. The community articulates the problem and shows an obvious demand and willingness to help. Meanwhile the member of Go core can’t manage to build even a single cohesive counterargument, but even so, the issue along with all its ideas and suggestions are summarily rejected.
Follow-up (2016/06/24): Russ Cox re-opened the issue (presumably after seeing complaints on Twitter given that the original maintainer had locked discussion on it) and it was subsequently resolved.
?w=1 on GitHub to hide whitespace changes helps mitigate this problem,
but isn’t the default, and doesn’t allow comments to be added.