Go, Mental Models, and Side Effects
I recently wrote about my struggle of learning Rust and Java at the same time. Since then, a lot of people have asked me if I finally came to grips with both programming languages. The short answer: yes and no. Yes, I do enjoy building things in Rust, although its compile times still demand considerable patience. And no, if I were given a choice, I wouldn’t use Java for my daily work, mainly because it feels heavy and tends to encourage programmers to write overly verbose code with too much abstraction, which goes against one of my core values.
One thing I’ve learned over the years – sometimes the hard way – is that learning a new language usually doesn’t happen in a vacuum. More often than not, there already exists a body of code, infrastructure, conventions, team values, etc. you need to grasp if you want to contribute in a meaningful way. This has led me to believe that the degree to which one enjoys a programming language invariably depends on the given environment. In other words, if you don’t like language X, it would be too easy – and certainly unfair – to put the sole blame on language X. Look around; X is only part of the equation.
That caveat aside, however, I still believe that some programming languages make it easier to write maintainable code than others. One excellent example is Go.
The virtues of Go
Go is a straightforward, no-frills programming language that is almost non-magical. You might even call Go boring, but being boring is actually a good thing when it comes to technology. After all, software should behave predictably and accomplish its goals without too many surprises. Code readability – and maintainability – first, language features second. What good is the latter without the former?
Go’s tooling is mature (okay, maybe except for dependency management). There’s a vast amount of working libraries, making it a perfect fit for developing delightful command-line tools, lightning-fast web servers, and robust distributed systems. Betting on Go, which is a breeze to learn, makes it easier for businesses to hire and onboard new developers (in particular if onboarding only involves telling the new employee where the source code can be found, but I digress).
Having used Go successfully in production for years, I could go on and on and on, but I’ll stop here.
Of course, Go is not a panacea. You can still write terrible code in it. More than once did I end up in interface hell when trying to navigate some Go code. Peter Bourgon also rightly notes that despite being a non-magical language, there are still a few ways magic can creep in through the use of global state – beware of unpredictable side effects!
Yet I’d argue that, by and large, Go’s explicitness and lack of fancy language features make it harder for programmers to create a Big Ball of Mud. Go is relatively easy to read, understand, and reason about. In other words, Go minimizes the effort required to build a mental model of a program.
What’s a mental model? I’m glad you’re asking.
Mental models 101
[A mental model] is a representation of the surrounding world, the relationships between its various parts and a person’s intuitive perception about his or her own acts and their consequences. Mental models can help shape behavior and set an approach to solving problems and doing tasks.
That’s what Wikipedia says. Here’s another explanation, this one specific to programming:
At a fundamental level, all software describes changes in the state of a system over time. Because the number states and state transitions in software can have combinatorial complexity, programmers necessarily rely on approximations of system behavior (called mental models) during development. The intent of a mental model is to allow programmers to reason accurately about the behavior of a system.
We all carry different, more or less accurate images of how something works in our heads, be it the weather or cars or code.
This goes for all engineering teams. Alice might be the one who knows the most about the new distributed cron solution (written in Go, of course). Hence she possesses the most comprehensive model of this particular component and how it interacts with other components. However, when it comes to service deployments and secrets management, Bob and Ted might have a clearer picture of what’s going on.
Systems are invisible to our eyes. The best thing we can do is understand them indirectly through mental models, and then perform actions based on these models. Unfortunately, these models tend to be incomplete – or just plain wrong. Blame complexity: the more complex a system, the more difficult it becomes for our brain to build a correct mental model of it.
Remember that I warned you about global state in Go? Well, in actuality, there are no side effects, just effects that result from our flawed understanding of the system.
Bugs occur because of an incomplete mental model:
Since mental models are approximations, they are sometimes incorrect, leading to aberrant behavior in software when it is developed on top of faulty assumptions…. The most sinister bugs occur when programmers falsely believe their mental models to be complete…. Throwing away the mental model is crucial to forming a sound hypothesis [when debugging].
Good programming languages can reduce the scope of the mental model developers must maintain, affecting both programming and debugging in a positive way. I believe Go meets the criteria. I miss using it on a daily basis.