Things I Like About Go

I have been writing a lot of code in Go lately and recently I was asked by someone why I write in Go. Being put on the spot, I didn't really have an answer, but it did get me thinking about why I like this language over the other languages I work with (Java, Ruby, Python, JavaScript). So without any further ado, here are some things I like about working with Go.

Fast compilation. One of the things that hooked me about interpreted languages (Python, Ruby) at first was the iteration speed. I could write and run the code without having to run a compilation step each time. While Go requires that compilation step (for most operations), the compilation is so fast that I don't even notice it as part of my workflow. And since Go is statically-typed, I'm not rerunning code constantly to verify that I got my types correct.

Easy JSON marshalling support. The bulk of software that I write uses JSON as a data transport format, so I'm constantly working with JSON all the time. But in Go it's almost like the JSON doesn't exist. When you define a struct with JSON labels, the json.Marshal and json.Unmarshal functions seemlessly handle the JSON marshalling without you having to write any JSON code at all. This is drastically different than working with JSON in languages like Python, where I have to constantly check whether the dict has a key or not, then check the contents for the existence of their keys… and it's turtles all the way down.

Single binary. My single most loved feature of Go is the fact that my application will compile down into a single binary, with all dependencies statically-linked inside of it. "Yeah but now the binary is huge!" is something I have heard. The binary may be large, but all I do is copy it to the target system and run it, no other dependency management is necessary. People tend to forget the size of the dependencies their application requires, they only count their application code. The reason I love this so much is that, throughout my career, the main frustration I've had is when it comes time to deployment. When a Ruby or Python application is deployed, there is always some dependency that needs to be downloaded and compiled for the system where the application will live. This process inevitably fails due to library conflicts, missing development libraries, or network failures. The deployment phase of a project is so overlooked, but Go handles it perfectly by bundling everything into a single, distributable binary. This is very similar to why Docker is so beloved by developers and administrators, you have one distributable artifact with every dependency baked inside.

Speed of Operation. Once my applications are compiled, running them is incredibly fast. There's no VM (Java, .NET) or interpreter (Python, Ruby) to invoke before the code is run, it's just native code. When I started writing Go I was sure I did something wrong, because the HTTP server I spun up just started, without the usual cold start lag that we developers expect when working with other languages.

Production Ready out of the Box. I love the fact that everything built into the standard library in Go is production ready. The HTTP server is multi-threaded and can handle as many resources as your system has available. I don't need to add Apache or Nginx in front, or bring in other production-grade libraries. This saves time, mental complexity, and frees me from deployment headaches.

Simple grammar. Go's grammar is surprisingly intuitive, and years ago it made me wonder if I could write complex applications with it. But the grammar made it so that I didn't need to memorize a bunch of different reserved words in the language. A list comprehension in Python is just a fancy for loop (it is; come at me bro), and I assume the Go language creators realized this too so they didn't put list comprehensions into Go. Same with map and filter, and other things that look nice on the surface but don't add much more than syntactic sugar. By using a small grammar set, Go allows you to keep more in your working memory (which is quite limited) at a time, and the trade-off is that I have to write slightly longer code. But that slightly longer code is just more explicit in its intention, which reduces the amount of magic in my code and increases readability.

No magic. When I first starting using Ruby in 2003 I loved the language (still do), and then when I started using Rails I felt so powerful. Then I started noticing all of the magic that Rails put into the code so that, if you push against the tool too hard, Rails pushes back and makes working with it difficult. I notice the same magic being pushed in other languages too. Spring Dependency Injection in Java in the early 00s, Python decorators, and so on. There's so much magic pushed onto developers that they start forgetting what is actually happening, and then when something bad happens they can't reason about the system. With Go, there's no magic that happens; you must call for something and then that something happens, very few side effects are possible. I can start at the beginning of my programs and work my way down into the bowels of the system without having to jump through hoops.

Simple mocking. I was a real skeptic at first when I saw how Go does interfaces. They looked too simple, and they lacked features that I expected from other languages (e.g. Java). And then one day I needed to mock out some AWS calls (Pro-Tip: don't test third-parties) and found that the simplicity of interfaces allowed for the simplest mocking I've seen in any language. I didn't need to use xUnit testing libraries or import some third-party mocking library that used reflection or monkey-patching. There was no magic to defining an interface, defining a struct that implements that interface, and then using that interface wherever I needed to call a third-party dependency. It gently nudged me into the proper form of dependency injection. It was magical, without requiring magic.

Multi-Platform artifacts from a single platform. While I have since switched over to Linux full-time, I primarily jump between two operating systems: MacOS and Linux. However, what has been standard is using Linux as the deployment target (i.e. application server). Go allows me to compile an artifact for any operating system without leaving my own system. I recall having to run VMs for other operating systems just so I could produce a binary, but now this process can be done on my machine and only takes an extra few seconds of processing (literally).

Enough batteries are included. One of the things that drew me to Python back in the 00s was that its mantra was "batteries included," which meant that, for most tasks, you didn't need to include third-party libraries to get things done. As the language matured, Python started to depend on third-party libraries while the standard library was becoming fractured (urllib, urllib2, urllib3 come to mind). But that's fine, Python was created in a different time (90s), when the Internet wasn't so prevalent, so I expect certain tasks, like networking, to feel a bit hacky. But with Go, which was borne in the beginning of Cloud-native programming, has none of these problems. It's standard library is full-featured for the majority of tasks that one would encounter in network programming or systems programming (my two main type of software development). I find that I rarely need to reach out for a third-party library and, when I do, it is completely out of necessity for my own preferences (e.g. I like --flag CLI flags and the flag library does not).

Stable versioning. I find that Go 1.x has been stable throughout its entire major version lifecycle. I know that the Go authors closed the 1.x language specification to new major features a lot time ago, but with 1.12 being released recently I find that moving between minor versions (up or down) never causes a compilation issue. Moving to newer minor versions always bring some sort performance optimization, but never introduces breaking changes like I've seen between Ruby and Python minor version changes. I appreciate that the Go authors chose to handcuff themselves because while being constrained like that can hurt innovation, it helps end users like myself understand the code easier between versions.

Decent package management. When I first started with Go, I found package management to be completely missing, and the go get, while useful, defaulted to downloading the HEAD from git repositories. It made creating deterministic builds almost impossible unless I vendored (saved) the third-party libraries to my project. Vendoring works, but it's not the best solution. Now that Go modules have been released, I moved completely over to the new module system and it's beautiful. No longer do I have a $GOROOT or $GOHOME, no longer do I pull git repositories from HEAD, and I have very deterministic builds. I found the Go module documentation to be quite cryptic, so Googling for examples and use cases helped immensely. Once the idea of modules clicked in my brain, I will never go back to the old vendoring format.

Automated formatting. At first I hated this feature because of past experience with egotistical developers beating me over the head with PEP8 (don't get me wrong: I like consistent styling but PEP8 has some stupid rules and we don't need to be robots and blindly follow every rule). I also disliked the use of tabs over spaces, and I still standby by argument that spaces are superior. But the best part of it is that I don't need to care that tabs are being used. I can use spaces all I want, then format my code to convert them to tabs. I don't care that tabs and spaces are mixed in the file while I'm developing because the formatter automatically converts all space indents to tabs. So while tabs vs spaces is still a religious war for some, Go makes it super easy to not think about it at all. Go's formatter doesn't make your code unreadable with idiosyncratic rules, it has a small set of rules that makes my code easier to read (aligning similar items, tabs for indents, organizing imports).

And that's it. I could mention other things I find interesting, like error handling or database connections, but they're not big enough of a deal for me (and some are laced with religious wars) that I'll just finish the list here.

Tags: programming, golang

« Previous: The Take-Home Test