Introduction to Go (golang), Part 1
If you read this blog for any amount of time you’ll know that I really like technology, specifically web development technologies. I’ve written about my experiences with Ruby on Rails, Batman, and a few curiosities along the way.
But my last post was something different, it was about Go. In a lot of ways it was the culmination of a journey, without trying to sound too weird, it was also the start of one.
Normally I don’t write about something until I feel like I have a decent grasp on it. So even though I started looking at Go over a year ago, I didn’t write anything about those experiences. At the beginning I was tempted to write about how Go was missing certain things…but that would’ve been premature. Now having used it for a bit longer, I understand it a lot better, though I still consider myself a novice and will likely be embarrassed about these posts in a few months (the perils of learning).
So now that I’ve taken a few more steps, I’m ready to write about getting started, so that other people coming from a similar background could skip the confusion and go straight to the good stuff….enjoying Go for the awesome language that it is today. So let’s get started!
Go is Different
Go is not what you think it is. It is not Java, It is not Ruby. It is not trying to be a language theorist’s dream. Go is not about type hierarchies. It is not about object-oriented programming or functional programming. Honestly, it has little to recommend it if you’re looking for something fashionable with all the buzzwords attached.
Go’s purpose is therefore not to do research into programming language design; it is to improve the working environment for its designers and their coworkers. Go is more about software engineering than programming language research. Or to rephrase, it is about language design in the service of software engineering. - Rob Pike
But Go is awesome, and you can get a lot done with it…you just have to learn it first. It’s tempting to approach new languages from the mindset of another language, but that will be most unhelpful here. It’s the easiest course to take…map a keyword from one language to the keyword of another, like a big Rosetta stone, but you will get frustrated pretty quickly. So before you do that, let’s look at where Go came from.
Being from a Physics background, I think it helps to start with first principles, so all the derivations make sense. Stated another way, Go was designed to solve a particular problem and the more we understand that, the more various design decisions will make sense.
The creation myth of Go is something like this: Rob Pike, Ken Thompson, and Robert Griesemer were waiting for a particularly long C++ build to take place when they decided to theorize a new language. What started on a whiteboard in 2007, shortly thereafter turned into a specification, which then was open sourced in 2009 with two compiler implementations. The article Go at Google: Language Design in the Service of Software Engineering provides an excellent overview of these things.
From this origin, we can discern a few problems that were explicitly set out to be solved by Go.
Problem one: Development Speed
The bane of compiled languages and the siren’s song of intepreted languages, slow builds are enough to drive anyone crazy. Once the build is slow enough, developer productivity goes out the window, as even simple changes can take forever to materialize. But optimizing for developer happiness (in the form of a fast dev cycle) generally has meant slow performance for the million or billion times that the code gets run in production. It’s a painful tradeoff.
Go enters the picture as a great compromise in this regard. The compiled code is super fast (see previous post) and the build times are negligible. Massive projects still have subsecond build times. How does it do it?
Well, let’s take the often complained about decision to have unused imports throw compile errors. This is intrinsic with this desire to go fast! The experience of the language designers on huge projects taught them that unnecessary includes resulted in ridiculous extraneous I/O during builds while imports would be included thousands of extra times. Throwing the error then is a friendly reminder from the compiler to take that trash out of your code so you can go fast.
The construction of a single C++ binary at Google can open and read hundreds of individual header files tens of thousands of times. In 2007, build engineers at Google instrumented the compilation of a major Google binary. The file contained about two thousand files that, if simply concatenated together, totaled 4.2 megabytes. By the time the #includes had been expanded, over 8 gigabytes were being delivered to the input of the compiler, a blow-up of 2000 bytes for every C++ source byte. - Rob Pike
This is also connected to the decision to not declare implements explicitly for interfaces. If you have to type
implements in all the spots that an interface is satisfied, that also means you have to include the “header” of the place where that interface is defined…once again, many more includes which would slow down the builds. Implicit satisfication of interfaces also results in more flexible code, which is itself more resilient to change, delaying interface ossification as long as possible (which is a good thing!).
If you didn’t know the above, you might be tempted to complain, “why do I have to add and delete the fmt package for debugging, how annoying!”, when in fact the language is preventing you from slowing down your builds. It isn’t intuitive, since we’ve become used to these very language “features” which ultimately undermine larger projects.
As this is the Internet, I feel compelled to throw in the YMMV caveat. If you know the gotchas of whatever language you’ve been using for years, you’ll be able to avoid many things simply due to your skill. I think Go helps the average developer not make these sort of mistakes (because they are compile errors). An important distiction to keep in mind, and one coherent with the language designers’ goals.
Problem two: Modern
The computing landscape today is almost unrelated to the environment in which the languages being used, mostly C++, Java, and Python, had been created. The problems introduced by multicore processors, networked systems, massive computation clusters, and the web programming model were being worked around rather than addressed head-on. - Rob Pike
C++ 1979. Python 1989. Java 1990. Ruby 1993. Let’s face it…that’s more than a few internet years. As a computer guy, I’m confident we could do our jobs with any sane language (having worked in Fortran77 for a short while, I can attest that you can build stuff with almost any tool), but the question of suitability of a language for a particular task should be considered.
How does that play out? For Go, I’d reference the concurrency model and it’s standard library. I want to write more about these in depth, so a brief aside will have to suffice here.
Let’s say you want to decompose a linear process into a concurrent one…one which can be parallelized if the running environment were to permit it. In Go, it’s this easy:
Now, there a many approaches to concurrency but they can be notoriously difficult to reason about. If you have had to debug a deadlock with mutexes, you don’t need me to elaborate on this. Go’s approach results in code that is easily understood as it can be reasoned about like a sequential program. That’s a huge plus.
The Go developers like to describe the standard library as a “batteries-included” library. What I think they mean by that is if you spin up a simple webserver like so:
that it will actually be awesome from the start (I’d say “webscale” but then I couldn’t finish writing the article for humiliation). This leads to one source of confusion for newbies who immediately ask for the “Ruby on Rails” of Go. There are plenty of great libraries that build upon the strong foundations of the Standard Library, but you can get away without having to use them. I admit, it was really weird for me at first that I could use a standard library and not a diadem-full of Gems to build something.
Ok, well I want to cover those again in the future, so for now, that will have to suffice.
Problem three: Complexity
Finally, I think the Go designers wanted to keep the language simple (simple in the usual definition of simple).
- How many loop keywords are there? 1
- Why Garbage Collection? Because it’s easier while working with concurrency.
- Why eliminate inheritance? Because it’s inflexible to change and composition is almost always better.
- How many keywords? 25. vs 50 in Java and 48 in C++ Expressiveness of Go
- Why CSP model for concurrency? Because mutexes and locking are a hard paradigm to get right.
Another way to see this is you can sit down and read the Language Specification in one sitting and have it all in your working memory. The concepts are “simple” enough that this is a feasible task. It’s actually really cool…one, that there is a language spec…nudge nudge…but primarily that it’s written in such comprehensible terms. I’ve had serveral sittings by the fire with the spec in one hand and a Lagavulin 16 in the other…and each time I appreciate the efforts of the developers in documenting their work.
If you’ve read this far, then you are most certainly a delightful person. I’ll write quite a bit more on Go, hopefully this was a helpful first step.