hetzel.net

My Go + SQLite Stack

Go Gopher Races Past PHP Elephant

Almost all my newer websites and services run on a very similar stack. Even before LLM agents, I was writing pretty much the same kind of code by hand. For Sprechkabine and one of my older merchandise shops, for example. We measure uptime in years.

Go

A very small, stable language that's easy to understand, easy to write, and easy to read. Just two hurdles that someone scarred by PHP might need to look up a couple of times: What are pointers? What are channels? Go is garbage-collected, so apart from optimizations or special cases, you generally don't have to worry about memory management.

The trick to escaping the Apache/PHP monoculture: Go's standard library comes with its own HTTP server. You can either let Go handle Let's Encrypt and TLS directly, or put another reverse proxy in front of it to take care of that. Nginx or Caddy are good choices.

The language can be executed like a script or compiled into a binary. The runtime is always included, so the entire program can usually run from a single small file.

You define your data model, routes, and handlers, and can use middleware to add authentication, logging, or whatever else you need to the request pipeline.

The standard library is built for the web. Practically all common protocols and standards are covered by mature, well-maintained code. Beyond the web, Go can interface with all sorts of C code, the operating system, and other CLI tools. I also have local scripts written in Go that I might otherwise have written in shell or Python.

Go comes from Google, has maintained backward compatibility for practically forever, is actively maintained, and is used by dozens of giant companies. Even if Google were to shut down, the language would almost certainly continue to be maintained.

SQLite

Alongside Curl, probably the most polished piece of software we've managed to produce in the last 40 years. If it's good enough for airplanes and Mars rovers, it's probably good enough for my blog.

SQLite isn't a separate database server. Instead, it's either compiled directly into the binary or linked as a system library provided by the operating system.

You can access SQLite from Go through its C interface (CGO), or use a Go port like modernc.org/sqlite, which comes with its own bugs and trade-offs but avoids the additional dependency on a C compiler and the longer build times.

For most websites, this is all you need: the binary, the database file, and perhaps an environment file containing secrets and configuration.

Templates

Go templates are a bit clunky. You can really taste Go's origins: old-school C compiler veterans who seem to enjoy making life difficult for themselves and the rest of us. With a little help from a chatbot, I've gotten the hang of them by now, but they're still somewhat awkward.

A bit closer to PHP is Templ. I haven't tried it myself yet, but it's considerably more human-readable.

If performance matters: Go is close to C, and fprintf may be completely unreadable, but it's also very, very fast for an application's hot path. In my case, that's things like podcast feeds that get hammered nonstop.

Templates and other static assets can easily be baked into the binary, keeping them consistent with the corresponding code commit.

Frontend

Vanilla JS, a bit of Bootstrap, whatever your heart desires. Server-side rendering eliminates a lot of state, bugs, and headaches from the browser. For a bit of interactivity, you can look at HTMX, which uses simple HTML attributes to replace parts of a webpage with new elements from the server and weighs just 14 KB.

Deployment

Plenty of options, from bare metal to Docker. For most of my services, I've now settled on a workflow using git push prod main. A bare repo on the server receives the new code, triggers a new build and a backup, saves the previous deployment for rollback, and checks whether the new binary starts correctly under systemd.

For the adventurous: Go can run as CGI-BIN, like it's 1999, or as WASM in Cloudflare Workers. Whatever floats your boat.

Backups

SQLite provides several ways to create a consistent snapshot for backup. I save one of these snapshots during deployment and keep, for example, seven of them around for potential rollbacks.

On the server, additional backup software like Restic or Rsync can ship further consistent backups of data and code elsewhere.

Scalability

As long as we're mostly reading from SQLite, no problem. Once we start writing more frequently or running larger database transactions, SQLite's single-writer limitation could become an issue. But at that point, upgrading to a "proper" database server isn't a huge leap. The database/sql interface helps with that.

Performance

With pprof, you can pinpoint exactly where your code is wasting CPU cycles. For example, the key optimization for my iOS push server was fixing inefficient JSON serialization through reflection. Simply specifying the types explicitly made the code run twice as fast. That was when we had just a handful of users. Now we have several thousand, and the code is still just as fast.

All my gazillion Go services now run on two of the smallest VMs at Hetzner, costing around 5 euros each. With virtually no significant RAM or CPU usage. The old Apache/PHP stack needed several times as much, just for Bits und so alone.

LLM

The migration of Bits und so from WordPress/PHP to Go was almost entirely automated, until Go's output was identical to the PHP code down to the last bit. The only catch, which I was already aware of beforehand: purchases involving external services are difficult to test and required some manual work.

Codex is only human, too. Funnily enough, it keeps running into exactly the same Go pitfalls that I do when coding by hand: unused imports, unused variables. Of course, there are plenty of other pitfalls, but across many tens of thousands of lines of code, it hasn't fallen into those yet.

The somewhat verbose if err != nil syntax, which you need everywhere, is repetitive, but leaves very little room for interpretation. Easy to write, easy to read. For humans and agents alike.