Ramblings of an aging IT geek
← Ramblings of an aging IT geek
golang

a hundred lines of go that just sit there and work

Shipping a small single-binary Go daemon for a niche homelab job, and why Go's deployment story made it worth doing properly rather than as a shell script.

A code editor showing Go source on screen

I shipped a tiny daemon this week and I'm absurdly pleased with it. It does one thing: watches a directory, and when a file lands that matches a pattern, it does a bit of processing and moves it on. The sort of job that has lived in a cron'd shell script on every machine I've ever owned. This time I wrote it in Go, and the difference in how it feels to operate is out of all proportion to the size of the code.

The whole thing is about a hundred and twenty lines. A fsnotify watcher, a worker that does the work, a context for clean shutdown so a SIGTERM doesn't leave a half-processed file behind. That last part is the bit the shell script never did properly. The shell version, if you killed it mid-run, left things in a state you had to clean up by hand on Monday.

ctx, stop := signal.NotifyContext(context.Background(),
    syscall.SIGINT, syscall.SIGTERM)
defer stop()

for {
    select {
    case <-ctx.Done():
        log.Println("shutting down cleanly")
        return
    case ev := <-watcher.Events:
        handle(ev)
    }
}

But the real reason I bothered isn't the code, it's the deployment. GOOS=linux GOARCH=amd64 go build and I have a single static binary with no runtime, no interpreter, no pip install that breaks because the box has the wrong Python. I scp one file to the server, drop in a systemd unit, and it runs. No dependency hell, no "works on my machine", because the machine is irrelevant. The binary brings everything it needs.

A close-up of source code on a monitor

The systemd unit is almost as small as the program:

[Service]
ExecStart=/usr/local/bin/watchd
Restart=on-failure
User=watchd

[Install]
WantedBy=multi-user.target

Restart=on-failure and a clean shutdown handler between them mean I genuinely don't think about it. It restarts if it dies, it shuts down without leaving a mess when I deploy, and the logs go to journald like everything else. I've checked on it twice since Boxing Day, both times out of curiosity rather than need, and both times it was sat there having quietly processed several hundred files without comment.

That's the bit I keep coming back to with Go for this kind of work. It is not a clever language and the program is not clever code. There's no generics yet, the error handling is verbose, half of it is if err != nil. But the result is a thing I can hand to a server and forget about, and "forget about" is the highest praise I have for a piece of infrastructure. The shell script worked too, most of the time. This one works all of the time, shuts down properly, and is one file. For something that's going to sit in a corner running for years, that's the trade I want.

A good way to end the year, honestly. Something small, finished, and shipped, that asks nothing further of me.