The Boring Stack


3 min read

Go, Because It Reads the Same in Five Years

The language decision behind every app I build

This is the first of ten posts on the decisions behind how I build software. Each one gets a row on my stack page, and each one is boring on purpose. The first decision is the one everything else depends on: the language.

I build in Go.

What I need from a language

I lead IT for a living and write software in the evenings and on weekends. That shapes what I need. A project might sit untouched for three months. When I come back, I need to understand it in minutes, change it safely and ship it the same evening.

So I don’t optimise for how fast I can write code on day one. I optimise for how fast I can read it on day ninety.

Why Go fits

One static binary. go build produces a single file that contains the whole app: the web server, the templates, the database migrations, even the CSS and fonts if you embed them. Deploying means copying one file. There’s no runtime to install on the server and no dependency folder to keep in sync.

A standard library that covers the web. HTTP server, routing, JSON, templating, cryptography, testing: it’s all in the standard library, maintained by the Go team, with a compatibility promise. Many apps need only a handful of dependencies beyond that.

Code that looks the same everywhere. gofmt settles formatting for every Go project in the world. The language has few features and almost no magic. Go code written by someone else looks very much like Go code written by me, and that includes the me of five years ago.

Errors you can see. Go’s error handling is famously repetitive: if err != nil after every call that can fail. I’ve come to like it. Every place where something can go wrong is visible in the code, right where it happens. Nothing gets thrown past three layers into a handler that doesn’t know what to do with it.

Boring upgrades. Go’s compatibility promise means code written for Go 1.0 still compiles today. Upgrading the Go version is usually a one-line change. Compare that with ecosystems where a major framework version means rewriting half of the app.

What it costs

Go isn’t free of trade-offs.

  • It’s verbose. Some things take more lines than in Python or TypeScript. I accept that, because the extra lines are the readable kind.
  • The ecosystem is smaller for some things. For web apps it’s rarely a problem. For data science or machine learning, Go isn’t the obvious choice.
  • Generics came late and are deliberately limited. If you love expressive type systems, Go will feel plain. Plain is the point.

Pinning the tools, too

Pinning the Go version isn’t enough. Tools like templ (for templates) and golangci-lint (for linting) generate or check code, and a different version of the tool can produce different results. A separately installed templ can generate calls to functions that the pinned library version doesn’t have, and a globally installed linter built with an older Go can fail on newer code.

Since Go 1.24, go.mod can pin tools with a tool directive, and go tool templ runs exactly that version. Every tool in my projects runs through go tool. The same commit builds the same way on every machine, which is what boring means.

When I’d choose differently

If a team already knows another language well and has the operations around it, I wouldn’t force Go on them. A boring stack is the one your team can run in its sleep. For me, working alone and in short sessions, that’s Go.

Next: why I don’t use a router framework.

More in The Boring Stack

Share