Go is boring. That's the point.

There is a particular kind of madness that sets in around year three of a large software project. The codebase has grown. The team has turned over twice. The original architects, the ones who made the really important decisions, have either been promoted into irrelevance or left for a startup that is definitely going to disrupt something. What remains is their legacy including a monument to everything they knew, everything they read about that one weekend, and every clever abstraction they were proud of at the time and has abused into utter disdain.

I have stood in front of codebases like this. I have created the function maps and over-overloaded operators saying, "it's exactly what we need for today." I have stared at template metaprogramming in C++ that would make a mathematician weep with admiration and a new hire weep for different reasons entirely. I have stared into the abyss of recursive expression tree transformations and source generators in C# that make debuggers trail into schizophrenic delusions. I have watched senior engineers spend forty minutes in a whiteboard session just debating what a piece of Perl code was trying to do before they could even begin to discuss whether it was doing it correctly. I have, on one memorable occasion, opened a Python file at two in the morning during an incident and found a decorator wrapping a decorator wrapping a class that monkey-patched a third-party library that was copy pasted and not even consumed properly as a dependency. I aged.

Go did not give me that. Not even once has a Go ecosystem done that to me. And I mean that as the highest possible compliment.

However, the first complaint you will hear from engineers encountering Go for the first time, especially those arriving from languages with richer type systems, is that it feels limited. There are no sum types. There is no pattern matching in the ML sense. Iterators are awkward and where's my lambda. The generics support, added late and somewhat grudgingly, is deliberately restrained. Error handling is explicit to the point that some people consider it verbose, which is a polite way of saying they have typed if err != nil enough times to develop opinions about it.

These observations are all correct. Go is missing things. The question worth asking is whether those things are missing the way a fine restaurant is missing a deep fryer, or missing the way your car is missing an extra cupholder.

The answer, for the vast majority of software being written today, is the former. Go was designed by people who had spent careers inside very large, very complicated systems, and who had noticed something that is easy to miss when you are young and excited about programming languages: the hardest part of software at scale is not expressiveness. It is legibility and the ability of a person who was not there when the code was written. Developers need to quickly understand what it does, change it safely, and go home at a reasonable hour.

Go optimizes ruthlessly for that. It does so by making the language small enough that there are only so many ways to say a thing. When the language limits how clever you can be, teams converge on similar solutions naturally. That convergence is worth more than almost any individual language feature and the correctness becomes almost reflexive. It is the difference between a codebase that a new engineer can contribute to in their first week and one that requires a guided tour from whoever is left with "there's some history here" stories no one wanted to hear.

Lets be honest about what most software actually is. It is services that talk to databases. It is APIs that sit between a frontend and some business logic that is typically limited to string interpolation and basic arithmetic. It is pipelines that move data from one place to another and hopefully transforms it correctly along the way. It is CLIs that automate the things that would otherwise require someone to remember a sequence of steps. It is the scaffolding of modern infrastructure, invisible and load-bearing and absolutely needing to work at three in the morning when someone is paging you.

This is not glamorous. It is also, by volume, essentially all of the software that keeps the world running. Go was built for precisely this category of work, and it is extraordinarily good at it. The compile times are fast enough that you do not lose your train of thought waiting for a build. The standard library covers most of what you need without pulling in a dependency that was last updated during a different presidential administration. The concurrency model, goroutines and channels, lets you write concurrent code that you can actually reason about, which is a sentence I have never been able to say about threading in most other languages without following it with several caveats and some hubris claiming, "skill issue."

Testing is a first-class citizen, not an afterthought bolted on by the community. The toolchain is opinionated enough that you do not spend your first two days on a new project configuring a formatter or arguing about where the braces go. Dependency tooling meets developers where they are, git. You write code and the code does what you said. You test the code. You ship the code. You sleep.

That is the 99%. It is not boring in the pejorative sense. It is boring in the sense that a well-built bridge is boring. It is boring in the sense that you do not want your infrastructure to be interesting. At least I don't.

Go is missing some things and I believe it's perfectly fine. Here is where I will be precise, because the argument deserves precision rather than hand-waving.

Go does not have sum types. A sum type, for the uninitiated, is a type that can be one of several distinct variants, each potentially carrying different data. Rust's Result and Option types are the canonical examples. They are genuinely useful. When you model a value that can be either a successful response or one of several typed errors, and the compiler forces you to handle each case, you catch entire categories of bugs at compile time that would otherwise surface at runtime in a production system.

This is real. I am not going to pretend otherwise. But consider the trade. Go's error handling is explicit and uniform. Every function that can fail returns an error. You handle it, or you propagate it, or you make a conscious decision to ignore it and live with that decision. The pattern is so consistent across the entire ecosystem that reading error handling code in a Go codebase you have never seen before is immediately familiar. There is no treasure hunt through custom error hierarchies or monad transformers to understand what happens when something goes wrong. It is right there, every time, in the same shape.

The same logic applies to pattern matching, macros, and most of the features that Go conspicuously lacks. Each of those features, used well, is powerful. Each of those features, used by someone who is smarter than the next person who has to maintain the code, is a liability. I've seen both too many times in projects that just didn't need it. You wish someone had taken away the keys to begin with by the time you're done refactoring.

And here is the thing about enterprise software... You do not get to choose how smart the next person is. You do not get to guarantee the wizard who designed an elegant type-level state machine will be available to explain it. What you can do is write code that gives the next person a fighting chance.

I also want to address the "right tool for the right job" framing directly, because it is often used as a way of avoiding the conversation rather than advancing it.

If you are writing an operating system kernel, Go is not the right tool. The memory model does not permit the control that work requires, and the garbage collector, however well-tuned, introduces latency characteristics that are unacceptable for that class of problem. C and Rust are genuinely better there. I mean that without irony or qualification. If you are doing heavy numerical computing, writing a game engine, or building software where you need to model complex domain logic with many mutually exclusive states and compiler-enforced exhaustiveness, there are languages better suited to those problems. The existence of Go does not erase the existence of those use cases.

But those use cases are, in the most literal sense, the exception. Most engineers working in most companies on most days are not writing OS kernels or GPU tuned graphics pipelines. They are writing the thing that needs to work, needs to be testable, needs to be handed off, and needs to keep working after the person who wrote it has moved on. For that work, Go's constraints are not a deficiency. They are load-bearing walls.

The developers who reach for a more expressive language for that category of problem are often optimizing for the pleasure of writing the code rather than the long-term cost of maintaining it. I understand and have perpetuated that impulse. I have felt and dealt it and made selfish decisions with the intent of learning something new. I have also been the person called in to maintain the result, and I have learned to weight those things very differently.

Speaking of maintaining, what does not come up enough in language discussions: the cost of onboarding is compounding. Maybe because I've moved into leadership and happen to still code. I have to weigh the costs to the business with the urge of myself and individuals to pad resume acronyms.

Every novel abstraction in a codebase is a thing the next engineer has to learn. Every clever use of a language feature that is not in the obvious path of the language's documentation is a question that will be asked in a code review, or worse, not asked and misunderstood. Every time a team has to hold a meeting to align on "how we do things here," that is time that is not being spent building things.

Go nearly eliminates this problem. Not because Go programmers are less creative, but because the language channels creativity into the architecture and the problem domain rather than into the implementation language itself. When a Go engineer joins a new team, they can read the codebase. Not skim it, not approximate it from context, but actually read it, because the range of ways to express any given idea in Go is small enough that it snaps into comprehension quickly.

Historical backing resonates so, I have watched teams of very senior Go engineers onboard junior engineers to meaningful contributions in a week. I've had good luck setting up interns for success with right sized problems in an easy to read Go codebase when the only code they've ever seen is Java in a data structures class. I have also spent a month just orienting a new hire in a sufficiently complex C++ codebase. The language is not the only variable in that comparison, but it is not a negligible one.

This is what I mean by tribal knowledge that does not require a tribe. In most languages, understanding a codebase depends heavily on understanding the specific conventions, idioms, patterns of the era, and implicit decisions of the people who built it. In Go, those things exist but they have a much narrower range. The language does most of the converging for you.

I did not always think this way. I spent years being the person who was frustrated by Go's missing features and the path dependencies. I wanted sum types, ternary operators, and richer pattern matching. I wanted the expressive power that I had in other languages, because I had used those features and I knew what they could do.

What changed was not a single moment but an accumulation of experiences on the other side of that ledger as an engineering leader. It was reading Go code written by someone else and understanding it immediately. It was inheriting a service that had been running for five years with minimal changes and being able to make a meaningful modification on my first day with it. It was watching a team of six people, with widely varying levels of experience, maintain a significant platform together without a dedicated language wizard whose departure would be a crisis.

Go's constraints are not the language's apology for lacking ambition. They are the language's thesis about where value actually lives in software development. The thesis is that most of the value is in the software working, being maintainable, and being transferable. Not in the elegance of the type system, not in the expressiveness of the abstractions, and not in the sophistication of the developer who wrote it.

The thesis is hard for me to argue against after two decades of enterprise software large and small with revenue impact and non-trivial security concerns.

Popular posts from this blog

The Fallacy of Cybersecurity by Backlog: Why Counting Patches Will Never Make You Secure

IPv6 White Paper I: Primer to Passive Discovery and Topology Inference in IPv6 Networks Using Neighbor Discovery Protocol

This is Cybermancy