Despite the fact that it's been around longer than I've been a programmer, I feel like the buzz around the Go programming language has increased of late.
This might be why, a few weeks ago, the client I'm working with announced their intention to start migrating portions of their Node microservices into Go. Since then I've spent some time learning Go, and pitching in on a Test Double open source project called Diplomat in order to get my bearings.
The process of learning a new language is, for me, a process of re-evaluating how I think. After almost five years in the land of JavaScript and Node, Go presented not only a challenge in terms of syntax, but in terms of worldview. Reflecting back on the experience, I went through three distinct phases of feeling, that influenced my ability to think and work in Go.
Phase 1: Confusion
The first stage of picking up a new language usually involves learning syntax: the low-level hows and whats that make up your average piece of code. How do you declare and assign variables? How do you iterate over collections of data, and what do those structures look like? How does concurrency work, if at all? I spent my first day or so working through the Tour of Go, and mapping the new syntax onto my pre-existing understanding of what code can do.
By "low-level", I do not mean simple, because the functionality of any two languages rarely forms a perfect overlap. I encountered new things that I couldn't easily link back to my JavaScript knowledge. I got tripped up by the interactions between var
, :=
and =
, spent a lot of unnecessary time trying to handle errors like they were exceptions, and managed to place one-way channel type arrows incorrectly every single time in a particularly rough pairing session.
At the same time, the puzzlement I experienced while learning these syntactical tricks paled in comparison to my first encounter with GOPATH
. I pulled an existing Go repository down from GitHub, tried to run it, and watched it fizzle anti-climactically. Which is when I learned that projects running Go 1.10 and lower have to be placed inside a particular, preset path on your machine, known as the GOPATH
. It meant putting code repositories outside the meticulous system I'd built in my home directory, which kept personal projects in a different space than client work. I felt an inward recoil at this restriction, and I wondered why developers were willing to accept it (as it turns out, they weren't entirely willing).
Fortunately, the recent introduction of Go modules has rendered that hassle unnecessary. Still, that particular experience, and my mental reaction, gave me a glimpse of the "box" I've been working in as a JavaScript developer. I'm used to doing things a certain way, and—more importantly—thinking about things a certain way. Looking back, the aspects of Go that challenged me the most were also those that challenged my JavaScript-y way of looking at the world. This was most noticeable in Go's restrictiveness: specifically, its type system and various compiler checks.
Phase 2: Frustration
The confusion about how Go works eventually led to the second emotional stage: getting frustrated with it. Since I'd been successful in other languages, it was easier to blame Go than my coding skills when things went wrong.
This is when I started working on Diplomat. I wasn't around at the inception of the codebase - another Test Double agent was the originator - and he'd provided a Makefile with a set of scripts to make jumping in easier. The main script, watch
, ran the formatting, build, testing, and linting steps on every file change. This is a lot of steps, which made for lots of opportunities for my terminal to shout at me.
And shout it did. If I left an unused variable in a function, it wouldn't run any tests until it was fixed. If the compiler failed in one part of the codebase, it wouldn't run unit tests for the other parts. It wouldn't run integration tests unless all the unit tests passed. I spent a non-trivial amount of that first day fighting with and against that watch
script. I still wasn't familiar with the compiler errors, and I had a bad habit of making several big changes between saves, both of which combined to make identifying errors difficult.
In retrospect, some of this pain was the natural outcome of figuring out a new language. But some of it was self-inflicted, a result of my trying to force my usual programming habits onto a language where they didn't quite fit. At one point during a pairing session, I made a change that led to open hanging channels. When the make
script output went blank, I restarted it. After doing this a couple of times, my pair suggested that make
probably wasn't the issue. He was right; the issue was that I didn't know what was wrong, and I was hoping that it was somehow the tooling's fault.
Phase 3: Curiosity
The transition out of the frustration phase occurred when I finally decided to stop fighting the Go tooling and work with it. At first, my inclination was to make big, sweeping changes and pick up the pieces later. Unfortunately the watch
script made this really difficult; because it contained all the build steps, messing up one thing essentially meant shutting down my entire feedback cycle, and I wasn't proficient enough in Go to put the pieces back together without help from the compiler. So I changed my approach.
I started making smaller changes, saving and checking the watch
output between each change. If something broke, I either went back or used the compiler output to tell me what to do next. That made it simpler to thread a single type change through the entire application, for example, instead of trying to make five type changes at once and blowing everything up. I was able to refactor a big component by building up a second one and using TDD to migrate behavior into it.
This felt strange, and it took a while for me to realize why. What I'd done was gone "back to basics" in terms of how I interact with a language. There was a time when I wrote JavaScript this way, using very disciplined TDD practices and taking advantage of tooling feedback. It took time to develop the confidence I have now in JavaScript, as well as the ability to model an application in my head without looking at any tools. Since I'd already been through this, I should have expected that it would take time to reach the same confidence with Go.
In the end, finding a solution to my frustrations led to increased curiosity, both about Go and about how I approach programming. I was able to ask better questions about how Go works, and how our packages should be architected to be understandable and easier to work with. I was engaging in a dialogue with the language and the tools, instead of trying to impose my will on them. At one point during a refactor we observed a repeated pattern throughout our packages, which we couldn't abstract away because Go doesn't support a generic type. This natural guardrail led me to reflect on why I was averse to duplicated code, and consider the complexity cost of compulsive DRY-ing. I realized that simply knocking my mind off of its usual tracks created space for curiosity about things that didn't specifically involve Go.
Conclusion
Some caveats: As you've probably guessed, this post isn't about teaching you to write Go. There are plenty of better teachers, some of which I've linked to above, and none of which are substitutes for writing some Go yourself. I also don't claim to know everything there is to know about Go, or that all of the decisions we've made in Diplomat are correct. There's always more to learn and understand. Someone else learning Go from JavaScript may experience entirely different phases than I did. This is simply a retrospective of a few weeks of learning Go, my state of mind, and lessons learned from picking up a new language.
Based on my experience, I'm inclined to think that the languages we use affect how we approach programming as a discipline, and that our perspectives can become fixed over time. Learning Go influenced me to approach problems from a different direction; asking how and why in an unfamiliar space prompted me to think about the answers to those questions on old terrain. I'm better poised to appreciate the nuances that make JavaScript and Go different as I work with both in the future.
The last few weeks have also given me pause to reflect on the perceived fluidity of what we call "coding skills" in our industry. It can be tempting to operate as though programming languages are interchangeable, and assume that software developers can flow between languages at any time. Which, unfortunately, isn't the case (as much as we'd like it to be). Developers, and humans in general, struggle to context switch between different activities. In a similar way, I think it's important to view a change of language as a change of context, and ensure that a team has the time and space to make their own journey to understanding.